Super Dev Mode: lots of I/O cleanup

(This is an internal cleanup that shouldn't have any external
effects.)

Introduced the Response interface to be used for all HTTP
responses. Replaced PageUtil with the Pages and Responses
classes. Removed I/O utility methods in PageUtil and replaced
them with Guava equivalents.

Change-Id: I528618377ecf106306ce6f926cabfd9be3f14277
Review-Link: https://gwt-review.googlesource.com/#/c/9361/
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
deleted file mode 100644
index 0e24d51..0000000
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
+++ /dev/null
@@ -1,249 +0,0 @@
-/*
- * Copyright 2012 Google Inc.
- *
- * 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.gwt.dev.codeserver;
-
-import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.TreeLogger.Type;
-import com.google.gwt.dev.json.JsonObject;
-import com.google.gwt.thirdparty.guava.common.base.Charsets;
-import com.google.gwt.thirdparty.guava.common.io.Files;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.net.URL;
-import java.util.Date;
-
-import javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Static utility methods for sending web pages.
- */
-class PageUtil {
-
-  private static final String TIME_IN_THE_PAST = "Fri, 01 Jan 1990 00:00:00 GMT";
-
-  /**
-   * Sends a page represented as a string.
-   */
-  static void sendString(String mimeType, String page, HttpServletResponse response)
-      throws IOException {
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType(mimeType);
-    response.getWriter().append(page);
-  }
-
-  /**
-   * Sets all headers that might help to prevent a page from being cached.
-   */
-  static void setNoCacheHeaders(HttpServletResponse response) {
-    response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
-    response.setHeader("Pragma", "no-cache");
-    response.setHeader("Expires", TIME_IN_THE_PAST);
-    response.setDateHeader("Date", new Date().getTime());
-  }
-
-  /**
-   * Sends an HTML page with some JSON code prepended to it.
-   *
-   * @param variableName  the name of the variable to set on the "window" object.
-   * @param json  the data to embed in the script.
-   * @param resourceName  the name of the HTML file to send (in the current directory)
-   * @param response  where to send the page
-   * @param logger  where to log errors (if any)
-   */
-  static void sendJsonAndHtml(String variableName, JsonObject json, String resourceName,
-      HttpServletResponse response, TreeLogger logger)
-      throws IOException {
-    URL resource = WebServer.class.getResource(resourceName);
-    if (resource == null) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      logger.log(TreeLogger.ERROR, "resource not found: " + resourceName);
-      return;
-    }
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType("text/html");
-
-    ServletOutputStream outBytes = response.getOutputStream();
-    Writer out = new OutputStreamWriter(outBytes, "UTF-8");
-
-    out.append("<!DOCTYPE html>\n");
-    out.append("<script>\n");
-    out.append("window." + variableName + " = ");
-    json.write(out);
-    out.append(";\n");
-    out.append("</script>\n");
-    out.flush();
-
-    copyStream(resource.openStream(), outBytes);
-  }
-
-  static void sendJsonAndJavaScript(String variableName, JsonObject json, String resourceName,
-      HttpServletResponse response, TreeLogger logger)
-      throws IOException {
-    URL resource = WebServer.class.getResource(resourceName);
-    if (resource == null) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      logger.log(TreeLogger.ERROR, "resource not found: " + resourceName);
-      return;
-    }
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType("application/javascript");
-
-    ServletOutputStream outBytes = response.getOutputStream();
-    Writer out = new OutputStreamWriter(outBytes, "UTF-8");
-
-    out.append("window." + variableName + " = ");
-    json.write(out);
-    out.append(";\n");
-    out.flush();
-
-    copyStream(resource.openStream(), outBytes);
-  }
-
-  static void sendFile(String mimeType, File file, HttpServletResponse response)
-      throws IOException {
-    BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
-    sendStream(mimeType, in, response);
-  }
-
-  /**
-   * Sends a text file, substituting one variable. (Doesn't preserve line endings.)
-   * @param templateVariable the string to replace
-   * @param replacement the replacement
-   */
-  static void sendTemplateFile(String mimeType, File file, String templateVariable,
-      String replacement, HttpServletResponse response) throws IOException {
-
-    BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
-    try {
-      response.setStatus(HttpServletResponse.SC_OK);
-      response.setContentType(mimeType);
-      PrintWriter out = response.getWriter();
-      while (true) {
-        String line = reader.readLine();
-        if (line == null) {
-          break;
-        }
-        line = line.replace(templateVariable, replacement);
-        out.println(line);
-      }
-    } finally {
-      reader.close();
-    }
-  }
-
-
-  /**
-   * Sends a page. Closes pageBytes when done.
-   */
-  static void sendStream(String mimeType, InputStream pageBytes, HttpServletResponse response)
-      throws IOException {
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType(mimeType);
-    copyStream(pageBytes, response.getOutputStream());
-  }
-
-  /**
-   * Copies in to out and closes in when done.
-   */
-  static void copyStream(InputStream in, OutputStream out) throws IOException {
-    try {
-      byte[] buffer = new byte[8 * 1024];
-      while (true) {
-        int bytesRead = in.read(buffer);
-        if (bytesRead == -1) {
-          return;
-        }
-        out.write(buffer, 0, bytesRead);
-      }
-    } finally {
-      in.close();
-    }
-  }
-
-  /**
-   * Reads a resource into a String.
-   */
-  static String loadResource(Class<?> base, String path) throws IOException {
-    InputStream resourceInputStream = base.getResourceAsStream(path);
-    if (resourceInputStream == null) {
-      throw new IOException("Resource " + path + " not found.");
-    }
-    ByteArrayOutputStream resourceBaos = new ByteArrayOutputStream();
-    copyStream(resourceInputStream, resourceBaos);
-    return resourceBaos.toString("UTF-8");
-  }
-
-  /**
-   * Reads a text file into a String.
-   */
-  static String loadFile(File file) throws IOException {
-    BufferedInputStream in = new BufferedInputStream(new FileInputStream(file));
-    try {
-      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-      copyStream(in, bytes);
-      return bytes.toString("UTF-8");
-    } finally {
-      in.close();
-    }
-  }
-
-  /**
-   * Writes a String to a file.
-   */
-  static void writeFile(String path, String content) throws IOException {
-    InputStream in = new ByteArrayInputStream(content.getBytes("UTF-8"));
-    OutputStream out = new FileOutputStream(path);
-    PageUtil.copyStream(in, out);
-  }
-
-  /**
-   * Sends an error response because something is unavailable. (Also logs it.)
-   */
-  static void sendUnavailable(HttpServletResponse response, TreeLogger logger, String message)
-      throws IOException {
-
-    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
-    response.setContentType("text/html");
-    HtmlWriter out = new HtmlWriter(response.getWriter());
-    out.startTag("html").nl();
-
-    out.startTag("head").nl();
-    out.startTag("title").text("Unavailable (GWT Code Server)").endTag("title").nl();
-    out.endTag("head").nl();
-
-    out.startTag("body").nl();
-    out.startTag("p").text(message).endTag("p");
-    out.endTag("body").nl();
-
-    out.endTag("html").nl();
-
-    logger.log(Type.INFO, "Sent unavailable response: " + message);
-  }
-}
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Pages.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Pages.java
new file mode 100644
index 0000000..9c1a704
--- /dev/null
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Pages.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.gwt.dev.codeserver;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.dev.json.JsonObject;
+import com.google.gwt.thirdparty.guava.common.io.Resources;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URL;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Common HTTP responses that send HTML pages. For non-HTML pages, see {@link Responses}.
+ */
+class Pages {
+
+  /**
+   * Returns an HTML page with some JSON code prepended to it.
+   * (If the resource file isn't found, returns an ErrorPage instead.)
+   *
+   * @param variableName  the name of the variable to set on the "window" object.
+   * @param json  the data to embed in the script.
+   * @param resourceName  the name of the HTML file to send (in the current directory)
+   */
+  static Response newHtmlPage(final String variableName, final JsonObject json,
+      final String resourceName) {
+
+    final URL resource = WebServer.class.getResource(resourceName);
+    if (resource == null) {
+      return new ErrorPage("resource not found: " + resourceName);
+    } else {
+      return new HtmlPage(variableName, json, resource);
+    }
+  }
+
+  /**
+   * A response that will send a static HTML page with JSON data prepended.
+   */
+  static class HtmlPage implements Response {
+    final String variableName;
+    final JsonObject json;
+    final URL resource;
+
+    /**
+     * @see {@link #newHtmlPage}.
+     */
+    private HtmlPage(String variableName, JsonObject json, URL resource) {
+      this.variableName = variableName;
+      this.json = json;
+      this.resource = resource;
+    }
+
+    @Override
+    public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+        throws IOException {
+      response.setStatus(HttpServletResponse.SC_OK);
+      response.setContentType("text/html");
+
+      ServletOutputStream outBytes = response.getOutputStream();
+      Writer out = new OutputStreamWriter(outBytes, "UTF-8");
+
+      out.append("<!DOCTYPE html>\n");
+      out.append("<script>\n");
+      out.append("window." + variableName + " = ");
+      json.write(out);
+      out.append(";\n");
+      out.append("</script>\n");
+      out.flush();
+
+      Resources.copy(resource, outBytes);
+    }
+  }
+
+  /**
+   * A response that will send an error page.
+   */
+  static class ErrorPage implements Response {
+    final int status;
+    final String message;
+
+    ErrorPage(String message) {
+      this(HttpServletResponse.SC_NOT_FOUND, message);
+    }
+
+    ErrorPage(int httpStatus, String message) {
+      this.status = httpStatus;
+      this.message = message;
+    }
+
+    @Override
+    public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+        throws IOException {
+
+      response.setStatus(status);
+      response.setContentType("text/html");
+      HtmlWriter out = new HtmlWriter(response.getWriter());
+      out.startTag("html").nl();
+
+      out.startTag("head").nl();
+      out.startTag("title").text("Unavailable (GWT Code Server)").endTag("title").nl();
+      out.endTag("head").nl();
+
+      out.startTag("body").nl();
+      out.startTag("p").text(message).endTag("p");
+      out.endTag("body").nl();
+
+      out.endTag("html").nl();
+
+      logger.log(Type.INFO, "Sent error page: " + message);
+    }
+  }
+}
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
index e84737f..7f07ca0 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -41,14 +41,17 @@
 import com.google.gwt.dev.resource.impl.ZipFileClassPathEntry;
 import com.google.gwt.dev.util.log.CompositeTreeLogger;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.thirdparty.guava.common.base.Charsets;
 import com.google.gwt.thirdparty.guava.common.base.Joiner;
 import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
 import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.google.gwt.thirdparty.guava.common.io.Files;
+import com.google.gwt.thirdparty.guava.common.io.Resources;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.Files;
+import java.net.URL;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -232,13 +235,14 @@
           compileLogger.log(Type.ERROR, "cannot create directory: " + parent);
           throw new UnableToCompleteException();
         }
-        Files.copy(publicResources.getResourceAsStream(pathName), file.toPath());
+        Files.asByteSink(file).writeFrom(publicResources.getResourceAsStream(pathName));
       }
 
       // Create a "module_name.nocache.js" that calculates the permutation and forces a recompile.
       String nocacheJs = generateModuleRecompileJs(module, compileLogger);
-      PageUtil.writeFile(outputDir.getCanonicalPath() + "/" + outputModuleName + ".nocache.js",
-          nocacheJs);
+      Files.write(nocacheJs,
+          new File(outputDir.getCanonicalPath() + "/" + outputModuleName + ".nocache.js"),
+          Charsets.UTF_8);
       writeRecompileNoCacheJs(outputDir, outputModuleName, nocacheJs, compileLogger);
     } catch (IOException e) {
       compileLogger.log(Type.ERROR, "Error creating stub compile directory.", e);
@@ -256,7 +260,8 @@
 
     String outputModuleName = module.getName();
     try {
-      String stub = PageUtil.loadResource(Recompiler.class, "recompile.nocache.js");
+      URL resource = Resources.getResource(Recompiler.class, "recompile.nocache.js");
+      String stub = Resources.toString(resource, Charsets.UTF_8);
       return "(function() {\n"
       + " var moduleName = '" + outputModuleName  + "';\n"
       + PropertiesUtil.generatePropertiesSnippet(module, compileLogger)
@@ -334,8 +339,9 @@
   private static void writeRecompileNoCacheJs(File outputDir, String moduleName, String content,
       TreeLogger compileLogger) throws UnableToCompleteException {
     try {
-      PageUtil.writeFile(outputDir.getCanonicalPath() + "/" + moduleName + ".recompile.nocache.js",
-          content);
+      Files.write(content,
+          new File(outputDir.getCanonicalPath() + "/" + moduleName + ".recompile.nocache.js"),
+          Charsets.UTF_8);
     } catch (IOException e) {
       compileLogger.log(Type.ERROR, "Can not write recompile.nocache.js", e);
       throw new UnableToCompleteException();
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Response.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Response.java
new file mode 100644
index 0000000..a52fc37
--- /dev/null
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Response.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.gwt.dev.codeserver;
+
+import com.google.gwt.core.ext.TreeLogger;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Encapsulates code that can send an HTTP response.
+ */
+interface Response {
+  void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+      throws IOException;
+}
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Responses.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Responses.java
new file mode 100644
index 0000000..d39a75e
--- /dev/null
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Responses.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * 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.gwt.dev.codeserver;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.codeserver.Pages.ErrorPage;
+import com.google.gwt.dev.json.JsonObject;
+import com.google.gwt.thirdparty.guava.common.base.Charsets;
+import com.google.gwt.thirdparty.guava.common.base.Preconditions;
+import com.google.gwt.thirdparty.guava.common.io.ByteStreams;
+import com.google.gwt.thirdparty.guava.common.io.Files;
+import com.google.gwt.thirdparty.guava.common.io.Resources;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Common HTTP responses other than HTML pages, which are in {@link Pages}.
+ */
+public class Responses {
+
+  private static final Pattern SAFE_CALLBACK =
+      Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*");
+
+  /**
+   * A HTTP response that sends a file.
+   */
+  static Response newFileResponse(final String mimeType, final File file) {
+    if (!file.isFile()) {
+      return new ErrorPage("file not found: " + file.toString());
+    }
+
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType(mimeType);
+        Files.copy(file, response.getOutputStream());
+      }
+    };
+  }
+
+  /**
+   * Returns a JSON response. If the request contains a _callback parameter, it will
+   * automatically be sent as a JSONP response. Otherwise, it's an AJAX response.
+   */
+  static Response newJsonResponse(final JsonObject json) {
+
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setHeader("Cache-control", "no-cache");
+        PrintWriter out = response.getWriter();
+
+        String callbackExpression = request.getParameter("_callback");
+        if (callbackExpression == null) {
+          // AJAX
+          response.setContentType("application/json");
+          json.write(out);
+        } else {
+          // JSONP
+          response.setContentType("application/javascript");
+          if (SAFE_CALLBACK.matcher(callbackExpression).matches()) {
+            out.print("/* API response */ " + callbackExpression + "(");
+            json.write(out);
+            out.println(");");
+          } else {
+            logger.log(TreeLogger.ERROR, "invalid callback: " + callbackExpression);
+            // Notice that we cannot execute the callback
+            out.print("alert('invalid callback parameter');\n");
+            json.write(out);
+          }
+        }
+      }
+    };
+  }
+
+  /**
+   * Sends a JavaScript file with some JSON data prepended to it.
+   * @param variableName the global variable where the JSON should be stored.
+   * @param json the data to include.
+   * @param resourceName the name of the JavaScript file.
+   */
+  static Response newJavascriptResponse(final String variableName, final JsonObject json,
+      final String resourceName) {
+
+    final URL resource = WebServer.class.getResource(resourceName);
+    if (resource == null) {
+      return new ErrorPage("resource not found: " + resourceName);
+    }
+
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("application/javascript");
+
+        ServletOutputStream outBytes = response.getOutputStream();
+        Writer out = new OutputStreamWriter(outBytes, "UTF-8");
+
+        out.append("window." + variableName + " = ");
+        json.write(out);
+        out.append(";\n");
+        out.flush();
+
+        Resources.copy(resource, outBytes);
+      }
+    };
+  }
+
+  /**
+   * Sends a text file, substituting one variable. (Doesn't preserve line endings.)
+   * @param templateVariable the string to replace
+   * @param replacement the replacement
+   */
+  static Response newTextTemplateResponse(final String mimeType, final File file,
+      final String templateVariable, final String replacement) {
+    if (!file.isFile()) {
+      return new ErrorPage("file not found: " + file.toString());
+    }
+
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        BufferedReader reader = Files.newReader(file, Charsets.UTF_8);
+        try {
+          response.setStatus(HttpServletResponse.SC_OK);
+          response.setContentType(mimeType);
+          PrintWriter out = response.getWriter();
+          while (true) {
+            String line = reader.readLine();
+            if (line == null) {
+              break;
+            }
+            line = line.replace(templateVariable, replacement);
+            out.println(line);
+          }
+        } finally {
+          reader.close();
+        }
+      }
+    };
+  }
+
+  /**
+   * Creates a page that sends the given stream of bytes.
+   * The response will close the stream after sending it.
+   * (Beware that if the page is never sent, the file handle will leak.)
+   * TODO: fix the callers and get rid of this.
+   */
+  static Response newBinaryStreamResponse(final String mimeType, final InputStream pageBytes) {
+    return new Response() {
+      boolean sent = false;
+
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        Preconditions.checkState(!sent);
+
+        try {
+          response.setStatus(HttpServletResponse.SC_OK);
+          response.setContentType(mimeType);
+          ByteStreams.copy(pageBytes, response.getOutputStream());
+        } finally {
+          pageBytes.close();
+        }
+        sent = true;
+      }
+    };
+  }
+
+  /**
+   * Wraps another response in order to log how long it takes to send it.
+   */
+  static Response newTimedResponse(final Response barePage, final String message) {
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        long startTime = System.currentTimeMillis();
+        barePage.send(request, response, logger);
+        long elapsedTime = System.currentTimeMillis() - startTime;
+        logger.log(TreeLogger.INFO, message + " in " + elapsedTime + " ms");
+      }
+    };
+  }
+}
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
index 16e1057..0adb6f5 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
@@ -17,6 +17,7 @@
 package com.google.gwt.dev.codeserver;
 
 import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.codeserver.Pages.ErrorPage;
 import com.google.gwt.dev.json.JsonObject;
 
 import java.io.BufferedReader;
@@ -93,8 +94,7 @@
     return SOURCEMAP_PATH + moduleName + "/__HASH__" + SOURCEMAP_URL_SUFFIX;
   }
 
-  void handle(String target, HttpServletRequest request, HttpServletResponse response,
-      TreeLogger logger)
+  Response handle(String target, HttpServletRequest request, TreeLogger logger)
       throws IOException {
     String moduleName = getModuleNameFromRequest(target);
     if (moduleName == null) {
@@ -103,35 +103,32 @@
 
     Outbox box = outboxes.findByOutputModuleName(moduleName);
     if (box == null) {
-      PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
-      return;
+      return new ErrorPage("No such module: " + moduleName);
     } else if (box.containsStubCompile()) {
-      PageUtil.sendUnavailable(response, logger, "This module hasn't been compiled yet.");
-      return;
+      return new ErrorPage("This module hasn't been compiled yet.");
     }
 
     String rootDir = SOURCEMAP_PATH + moduleName + "/";
     String rest = target.substring(rootDir.length());
 
     if (rest.isEmpty()) {
-      sendDirectoryListPage(box, response, logger);
+      return makeDirectoryListPage(box);
     } else if (rest.equals("gwtSourceMap.json")) {
       // This URL is no longer used by debuggers (we use the strong name) but is used for testing.
       // It's useful not to need the strong name to download the sourcemap.
       // (But this only works when there is one permutation.)
-      sendSourceMap(moduleName, box.findSourceMapForOnePermutation(), request, response, logger);
+      return makeSourceMapPage(moduleName, box.findSourceMapForOnePermutation(), request);
     } else if (rest.endsWith("/")) {
-      sendFileListPage(box, rest, response, logger);
+      return sendFileListPage(box, rest);
     } else if (rest.endsWith(".java")) {
-      sendSourceFile(box, rest, request.getQueryString(), response, logger);
+      return makeSourcePage(box, rest, request.getQueryString(), logger);
     } else {
       String strongName = getStrongNameFromSourcemapFilename(rest);
       if (strongName != null) {
         File sourceMap = box.findSourceMap(strongName).getAbsoluteFile();
-        sendSourceMap(moduleName, sourceMap, request, response, logger);
+        return makeSourceMapPage(moduleName, sourceMap, request);
       } else {
-        response.sendError(HttpServletResponse.SC_NOT_FOUND);
-        logger.log(TreeLogger.WARN, "returned not found for request: " + target);
+        return new ErrorPage("page not found");
       }
     }
   }
@@ -146,10 +143,8 @@
     return matcher.matches() ? matcher.group(1) : null;
   }
 
-  private void sendSourceMap(String moduleName, File sourceMap, HttpServletRequest request,
-      HttpServletResponse response, TreeLogger logger) throws IOException {
-
-    long startTime = System.currentTimeMillis();
+  private Response makeSourceMapPage(final String moduleName, File sourceMap,
+      HttpServletRequest request) {
 
     // Stream the file, substituting the sourceroot variable with the filename.
     // (This is more efficient than parsing the file as JSON.)
@@ -161,104 +156,103 @@
     String sourceRoot = String.format("http://%s:%d/sourcemaps/%s/", request.getServerName(),
         request.getServerPort(), moduleName);
 
-    PageUtil.sendTemplateFile("application/json", sourceMap,
+    final Response barePage = Responses.newTextTemplateResponse("application/json", sourceMap,
         "\"" + SOURCEROOT_TEMPLATE_VARIABLE + "\"",
-        "\"" + sourceRoot + "\"", response);
+        "\"" + sourceRoot + "\"");
 
-    long elapsedTime = System.currentTimeMillis() - startTime;
-
-    logger.log(TreeLogger.INFO, "sent source map for module '" + moduleName +
-        "' in " + elapsedTime + " ms");
+    // Wrap it in another response to time how long it takes.
+    return Responses.newTimedResponse(barePage, "sent source map for module '" + moduleName + "'");
   }
 
-  private void sendDirectoryListPage(Outbox box, HttpServletResponse response,
-      TreeLogger logger) throws IOException {
-
+  private Response makeDirectoryListPage(Outbox box) throws IOException {
     SourceMap map = SourceMap.load(box.findSourceMapForOnePermutation());
     JsonObject json = exporter.exportSourceMapDirectoryListVars(box, map);
-    PageUtil.sendJsonAndHtml("config", json, "directorylist.html", response, logger);
+    return Pages.newHtmlPage("config", json, "directorylist.html");
   }
 
-  private void sendFileListPage(Outbox box, String rest, HttpServletResponse response,
-      TreeLogger logger) throws IOException {
+  private Response sendFileListPage(Outbox box, String rest) throws IOException {
 
     SourceMap map = SourceMap.load(box.findSourceMapForOnePermutation());
     JsonObject json = exporter.exportSourceMapFileListVars(box, map, rest);
-    PageUtil.sendJsonAndHtml("config", json, "filelist.html", response, logger);
+    return Pages.newHtmlPage("config", json, "filelist.html");
   }
 
   /**
-   * Sends an HTTP response containing a Java source. It will be sent as plain text by default,
+   * Returns a page displaying a Java source file. It will be sent as plain text by default,
    * or as HTML if the query string is equal to "html".
    */
-  private void sendSourceFile(Outbox box, String sourcePath, String query,
-      HttpServletResponse response, TreeLogger logger) throws IOException {
-    InputStream pageBytes = box.openSourceFile(sourcePath);
+  private Response makeSourcePage(Outbox box, String sourcePath, String query, TreeLogger logger)
+      throws IOException {
 
+    InputStream pageBytes = box.openSourceFile(sourcePath);
     if (pageBytes == null) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      logger.log(TreeLogger.WARN, "unknown source file: " + sourcePath);
-      return;
+      return new ErrorPage("unknown source file: " + sourcePath);
     }
 
     if (query != null && query.equals("html")) {
-      BufferedReader reader = new BufferedReader(new InputStreamReader(pageBytes));
-      sendSourceFileAsHtml(box, sourcePath, reader, response, logger);
+      return makeHtmlSourcePage(box, sourcePath, pageBytes, logger);
     } else {
-      PageUtil.sendStream("text/plain", pageBytes, response);
+      return Responses.newBinaryStreamResponse("text/plain", pageBytes);
     }
   }
 
   /**
-   * Sends an HTTP response containing Java source rendered as HTML. The lines of source
+   * Returns a page that will send a Java source file as HTML. The lines of source
    * that have corresponding JavaScript will be highlighted (as determined by reading the
    * source map).
    */
-  private void sendSourceFileAsHtml(Outbox box, String sourcePath, BufferedReader lines,
-      HttpServletResponse response, TreeLogger logger) throws IOException {
+  private Response makeHtmlSourcePage(Outbox box, final String sourcePath,
+      final InputStream pageBytes, TreeLogger logger) throws IOException {
 
-    ReverseSourceMap sourceMap = ReverseSourceMap.load(logger,
+    final ReverseSourceMap sourceMap = ReverseSourceMap.load(logger,
         box.findSourceMapForOnePermutation());
 
-    File sourceFile = new File(sourcePath);
+    final File sourceFile = new File(sourcePath);
 
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType("text/html");
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("text/html");
 
-    HtmlWriter out = new HtmlWriter(response.getWriter());
-    out.startTag("html").nl();
-    out.startTag("head").nl();
-    out.startTag("title").text(sourceFile.getName() + " (GWT Code Server)").endTag("title").nl();
-    out.startTag("style").nl();
-    out.text(".unused { color: grey; }").nl();
-    out.text(".used { color: black; }").nl();
-    out.text(".title { margin-top: 0; }").nl();
-    out.endTag("style").nl();
-    out.endTag("head").nl();
-    out.startTag("body").nl();
+        HtmlWriter out = new HtmlWriter(response.getWriter());
+        out.startTag("html").nl();
+        out.startTag("head").nl();
+        out.startTag("title").text(sourceFile.getName() + " (GWT Code Server)").endTag("title").nl();
+        out.startTag("style").nl();
+        out.text(".unused { color: grey; }").nl();
+        out.text(".used { color: black; }").nl();
+        out.text(".title { margin-top: 0; }").nl();
+        out.endTag("style").nl();
+        out.endTag("head").nl();
+        out.startTag("body").nl();
 
-    out.startTag("a", "href=", ".").text(sourceFile.getParent()).endTag("a").nl();
-    out.startTag("h1", "class=", "title").text(sourceFile.getName()).endTag("h1").nl();
+        out.startTag("a", "href=", ".").text(sourceFile.getParent()).endTag("a").nl();
+        out.startTag("h1", "class=", "title").text(sourceFile.getName()).endTag("h1").nl();
 
-    out.startTag("pre", "class=", "unused").nl();
-    try {
-      int lineNumber = 1;
-      for (String line = lines.readLine(); line != null; line = lines.readLine()) {
-        if (sourceMap.appearsInJavaScript(sourcePath, lineNumber)) {
-          out.startTag("span", "class=", "used").text(line).endTag("span").nl();
-        } else {
-          out.text(line).nl();
+        out.startTag("pre", "class=", "unused").nl();
+
+        BufferedReader lines = new BufferedReader(new InputStreamReader(pageBytes));
+        try {
+          int lineNumber = 1;
+          for (String line = lines.readLine(); line != null; line = lines.readLine()) {
+            if (sourceMap.appearsInJavaScript(sourcePath, lineNumber)) {
+              out.startTag("span", "class=", "used").text(line).endTag("span").nl();
+            } else {
+              out.text(line).nl();
+            }
+            lineNumber++;
+          }
+
+        } finally {
+          lines.close();
         }
-        lineNumber++;
+        out.endTag("pre").nl();
+
+        out.endTag("body").nl();
+        out.endTag("html").nl();
       }
-
-    } finally {
-      lines.close();
-    }
-    out.endTag("pre").nl();
-
-    out.endTag("body").nl();
-    out.endTag("html").nl();
+    };
   }
-
 }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
index 979e13f..f0a359c 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -19,7 +19,10 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.TreeLogger.Type;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.codeserver.Pages.ErrorPage;
 import com.google.gwt.dev.json.JsonObject;
+import com.google.gwt.thirdparty.guava.common.base.Charsets;
+import com.google.gwt.thirdparty.guava.common.io.Files;
 
 import org.eclipse.jetty.http.MimeTypes;
 import org.eclipse.jetty.io.Buffer;
@@ -36,7 +39,7 @@
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.PrintWriter;
+import java.util.Date;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
@@ -80,15 +83,14 @@
   static final Pattern SAFE_FILE_PATH =
       Pattern.compile("/(" + SAFE_DIRECTORY + "/)+" + SAFE_FILENAME + "$");
 
-  private static final Pattern SAFE_CALLBACK =
-      Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*");
-
   static final Pattern STRONG_NAME = Pattern.compile("[\\dA-F]{32}");
 
   private static final Pattern CACHE_JS_FILE = Pattern.compile("/(" + STRONG_NAME + ").cache.js$");
 
   private static final MimeTypes MIME_TYPES = new MimeTypes();
 
+  private static final String TIME_IN_THE_PAST = "Fri, 01 Jan 1990 00:00:00 GMT";
+
   private final SourceHandler handler;
   private final JsonExporter jsonExporter;
   private final OutboxTable outboxes;
@@ -160,50 +162,56 @@
   }
 
   private void handleRequest(String target, HttpServletRequest request,
-      HttpServletResponse response, TreeLogger logger)
+      HttpServletResponse response, TreeLogger parentLogger)
       throws IOException {
 
     if (request.getMethod().equalsIgnoreCase("get")) {
-      doGet(target, request, response, logger);
+
+      TreeLogger logger = parentLogger.branch(Type.TRACE, "GET " + target);
+
+      Response page = doGet(target, request, logger);
+      if (page == null) {
+        logger.log(Type.WARN, "not handled: " + target);
+        return;
+      }
+
+      setHandled(request);
+      if (!target.endsWith(".cache.js")) {
+        // Make sure IE9 doesn't cache any pages.
+        // (Nearly all pages may change on server restart.)
+        response.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
+        response.setHeader("Pragma", "no-cache");
+        response.setHeader("Expires", TIME_IN_THE_PAST);
+        response.setDateHeader("Date", new Date().getTime());
+      }
+      page.send(request, response, logger);
     }
   }
 
-  private void doGet(String target, HttpServletRequest request, HttpServletResponse response,
-      TreeLogger parentLogger)
+  /**
+   * Returns the page that should be sent in response to a GET request, or null for no response.
+   */
+  private Response doGet(String target, HttpServletRequest request, TreeLogger logger)
       throws IOException {
 
-    TreeLogger logger = parentLogger.branch(Type.TRACE, "GET " + target);
-
-    if (!target.endsWith(".cache.js")) {
-      // Make sure IE9 doesn't cache any pages.
-      // (Nearly all pages may change on server restart.)
-      PageUtil.setNoCacheHeaders(response);
-    }
-
     if (target.equals("/")) {
-      setHandled(request);
       JsonObject json = jsonExporter.exportFrontPageVars();
-      PageUtil.sendJsonAndHtml("config", json, "frontpage.html", response, logger);
-      return;
+      return Pages.newHtmlPage("config", json, "frontpage.html");
     }
 
     if (target.equals("/dev_mode_on.js")) {
-      setHandled(request);
       JsonObject json = jsonExporter.exportDevModeOnVars();
-      PageUtil.sendJsonAndJavaScript("__gwt_codeserver_config", json, "dev_mode_on.js", response,
-          logger);
-      return;
+      return Responses.newJavascriptResponse("__gwt_codeserver_config", json,
+          "dev_mode_on.js");
     }
 
     // Recompile on request from the bookmarklet.
     // This is a GET because a bookmarklet can call it from a different origin (JSONP).
     if (target.startsWith("/recompile/")) {
-      setHandled(request);
       String moduleName = target.substring("/recompile/".length());
       Outbox box = outboxes.findByOutputModuleName(moduleName);
       if (box == null) {
-        PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
-        return;
+        return new ErrorPage("No such module: " + moduleName);
       }
 
       // We are passing properties from an unauthenticated GET request directly to the compiler.
@@ -216,43 +224,36 @@
       runner.submit(job);
       Job.Result result = job.waitForResult();
       JsonObject json = jsonExporter.exportRecompileResponse(result);
-      sendJsonResult(json, request, response, logger);
-      return;
+      return Responses.newJsonResponse(json);
     }
 
     if (target.startsWith("/log/")) {
-      setHandled(request);
       String moduleName = target.substring("/log/".length());
       Outbox box = outboxes.findByOutputModuleName(moduleName);
       if (box == null) {
-        PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
+        return new ErrorPage("No such module: " + moduleName);
       } else if (box.containsStubCompile()) {
-        PageUtil.sendUnavailable(response, logger, "This module hasn't been compiled yet.");
+        return new ErrorPage("This module hasn't been compiled yet.");
       } else {
-        sendLogPage(box, response);
+        return makeLogPage(box);
       }
-      return;
     }
 
     if (target.equals("/favicon.ico")) {
       InputStream faviconStream = getClass().getResourceAsStream("favicon.ico");
-      if (faviconStream != null) {
-        setHandled(request);
-        // IE8 will not load the favicon in an img tag with the default MIME type,
-        // so use "image/x-icon" instead.
-        PageUtil.sendStream("image/x-icon", faviconStream, response);
+      if (faviconStream == null) {
+        return new ErrorPage("icon not found");
       }
-      return;
+      // IE8 will not load the favicon in an img tag with the default MIME type,
+      // so use "image/x-icon" instead.
+      return Responses.newBinaryStreamResponse("image/x-icon", faviconStream);
     }
 
     if (target.equals("/policies/")) {
-      setHandled(request);
-      sendPolicyIndex(response);
-      return;
+      return makePolicyIndexPage();
     }
 
     if (target.equals("/progress")) {
-      setHandled(request);
       // TODO: return a list of progress objects here, one for each job.
       JobEvent event = eventTable.getCompilingJobEvent();
 
@@ -263,228 +264,229 @@
       } else {
         json = jsonExporter.exportProgressResponse(event);
       }
-      sendJsonResult(json, request, response, logger);
-      return;
+      return Responses.newJsonResponse(json);
     }
 
     Matcher matcher = SAFE_MODULE_PATH.matcher(target);
     if (matcher.matches()) {
-      setHandled(request);
-      sendModulePage(matcher.group(1), response, logger);
-      return;
+      return makeModulePage(matcher.group(1));
     }
 
     matcher = SAFE_DIRECTORY_PATH.matcher(target);
-    if (matcher.matches() && handler.isSourceMapRequest(target)) {
-      setHandled(request);
-      handler.handle(target, request, response, logger);
-      return;
+    if (matcher.matches() && SourceHandler.isSourceMapRequest(target)) {
+      return handler.handle(target, request, logger);
     }
 
     matcher = SAFE_FILE_PATH.matcher(target);
     if (matcher.matches()) {
-      setHandled(request);
-      if (handler.isSourceMapRequest(target)) {
-        handler.handle(target, request, response, logger);
-        return;
+      if (SourceHandler.isSourceMapRequest(target)) {
+        return handler.handle(target, request, logger);
       }
       if (target.startsWith("/policies/")) {
-        sendPolicyFile(target, response, logger);
-        return;
+        return makePolicyFilePage(target);
       }
-      sendOutputFile(target, request, response, logger);
-      return;
+      return makeCompilerOutputPage(target);
     }
 
     logger.log(TreeLogger.WARN, "ignored get request: " + target);
+    return null; // not handled
   }
 
-  private void sendOutputFile(String target, HttpServletRequest request,
-      HttpServletResponse response, TreeLogger logger) throws IOException {
+  /**
+   * Returns a file that the compiler wrote to its war directory.
+   */
+  private Response makeCompilerOutputPage(String target) throws IOException {
 
     int secondSlash = target.indexOf('/', 1);
     String moduleName = target.substring(1, secondSlash);
     Outbox box = outboxes.findByOutputModuleName(moduleName);
     if (box == null) {
-      PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
-      return;
+      return new ErrorPage("No such module: " + moduleName);
     }
 
+    final String contentEncoding;
     File file = box.getOutputFile(target);
     if (!file.isFile()) {
       // perhaps it's compressed
       file = box.getOutputFile(target + ".gz");
       if (!file.isFile()) {
-        response.sendError(HttpServletResponse.SC_NOT_FOUND);
-        logger.log(TreeLogger.WARN, "not found: " + file.toString());
-        return;
+        return new ErrorPage("not found: " + file.toString());
       }
-      if (!request.getHeader("Accept-Encoding").contains("gzip")) {
-        response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
-        logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing");
-        return;
-      }
-      response.setHeader("Content-Encoding", "gzip");
+      contentEncoding = "gzip";
+    } else {
+      contentEncoding = null;
     }
 
+    final String sourceMapUrl;
     Matcher match = CACHE_JS_FILE.matcher(target);
     if (match.matches()) {
       String strongName = match.group(1);
       String template = SourceHandler.sourceMapLocationTemplate(moduleName);
-      String sourceMapUrl = template.replace("__HASH__", strongName);
-      response.setHeader("X-SourceMap", sourceMapUrl);
+      sourceMapUrl = template.replace("__HASH__", strongName);
+    } else {
+      sourceMapUrl = null;
     }
-    response.setHeader("Access-Control-Allow-Origin", "*");
+
     String mimeType = guessMimeType(target);
-    PageUtil.sendFile(mimeType, file, response);
+    final Response barePage = Responses.newFileResponse(mimeType, file);
+
+    // Wrap the response to send the extra headers.
+    return new Response() {
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        // TODO: why do we need this? Looks like Ray added it a long time ago.
+        response.setHeader("Access-Control-Allow-Origin", "*");
+
+        if (sourceMapUrl != null) {
+          response.setHeader("X-SourceMap", sourceMapUrl);
+          response.setHeader("SourceMap", sourceMapUrl);
+        }
+
+        if (contentEncoding != null) {
+          if (!request.getHeader("Accept-Encoding").contains("gzip")) {
+            response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+            logger.log(TreeLogger.WARN, "client doesn't accept gzip; bailing");
+            return;
+          }
+          response.setHeader("Content-Encoding", "gzip");
+        }
+
+        barePage.send(request, response, logger);
+      }
+    };
   }
 
-  private void sendModulePage(String moduleName, HttpServletResponse response, TreeLogger logger)
-      throws IOException {
+  private Response makeModulePage(String moduleName) throws IOException {
     Outbox box = outboxes.findByOutputModuleName(moduleName);
     if (box == null) {
-      PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
-      return;
+      return new ErrorPage("No such module: " + moduleName);
     }
 
     JsonObject json = jsonExporter.exportModulePageVars(box);
-    PageUtil.sendJsonAndHtml("config", json, "modulepage.html", response, logger);
+    return Pages.newHtmlPage("config", json, "modulepage.html");
   }
 
-  private void sendPolicyIndex(HttpServletResponse response) throws IOException {
+  private Response makePolicyIndexPage() {
 
-    response.setContentType("text/html");
+    return new Response() {
 
-    HtmlWriter out = new HtmlWriter(response.getWriter());
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        response.setContentType("text/html");
 
-    out.startTag("html").nl();
-    out.startTag("head").nl();
-    out.startTag("title").text("Policy Files").endTag("title").nl();
-    out.endTag("head");
-    out.startTag("body");
+        HtmlWriter out = new HtmlWriter(response.getWriter());
 
-    out.startTag("h1").text("Policy Files").endTag("h1").nl();
+        out.startTag("html").nl();
+        out.startTag("head").nl();
+        out.startTag("title").text("Policy Files").endTag("title").nl();
+        out.endTag("head");
+        out.startTag("body");
 
-    for (Outbox box : outboxes.getOutboxes()) {
-      File manifest = box.getExtraFile("rpcPolicyManifest/manifest.txt");
-      if (manifest.isFile()) {
-        out.startTag("h2").text(box.getOutputModuleName()).endTag("h2").nl();
+        out.startTag("h1").text("Policy Files").endTag("h1").nl();
 
-        out.startTag("table").nl();
-        String text = PageUtil.loadFile(manifest);
-        for (String line : text.split("\n")) {
-          line = line.trim();
-          if (line.isEmpty() || line.startsWith("#")) {
-            continue;
+        for (Outbox box : outboxes.getOutboxes()) {
+          File manifest = box.getExtraFile("rpcPolicyManifest/manifest.txt");
+          if (manifest.isFile()) {
+            out.startTag("h2").text(box.getOutputModuleName()).endTag("h2").nl();
+
+            out.startTag("table").nl();
+            String text = Files.toString(manifest, Charsets.UTF_8);
+            for (String line : text.split("\n")) {
+              line = line.trim();
+              if (line.isEmpty() || line.startsWith("#")) {
+                continue;
+              }
+              String[] fields = line.split(", ");
+              if (fields.length < 2) {
+                continue;
+              }
+
+              String serviceName = fields[0];
+              String policyFileName = fields[1];
+
+              String serviceUrl = SourceHandler.SOURCEMAP_PATH + box.getOutputModuleName() + "/" +
+                  serviceName.replace('.', '/') + ".java";
+              String policyUrl = "/policies/" + policyFileName;
+
+              out.startTag("tr");
+
+              out.startTag("td");
+              out.startTag("a", "href=", serviceUrl).text(serviceName).endTag("a");
+              out.endTag("td");
+
+              out.startTag("td");
+              out.startTag("a", "href=", policyUrl).text(policyFileName).endTag("a");
+              out.endTag("td");
+
+              out.endTag("tr").nl();
+            }
+            out.endTag("table").nl();
           }
-          String[] fields = line.split(", ");
-          if (fields.length < 2) {
-            continue;
-          }
-
-          String serviceName = fields[0];
-          String policyFileName = fields[1];
-
-          String serviceUrl = SourceHandler.SOURCEMAP_PATH + box.getOutputModuleName() + "/" +
-              serviceName.replace('.', '/') + ".java";
-          String policyUrl = "/policies/" + policyFileName;
-
-          out.startTag("tr");
-
-          out.startTag("td");
-          out.startTag("a", "href=", serviceUrl).text(serviceName).endTag("a");
-          out.endTag("td");
-
-          out.startTag("td");
-          out.startTag("a", "href=", policyUrl).text(policyFileName).endTag("a");
-          out.endTag("td");
-
-          out.endTag("tr").nl();
         }
-        out.endTag("table").nl();
-      }
-    }
 
-    out.endTag("body").nl();
-    out.endTag("html").nl();
+        out.endTag("body").nl();
+        out.endTag("html").nl();
+      }
+    };
   }
 
-  private void sendPolicyFile(String target, HttpServletResponse response, TreeLogger logger)
-      throws IOException {
+  private Response makePolicyFilePage(String target) throws IOException {
 
     int secondSlash = target.indexOf('/', 1);
     if (secondSlash < 1) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      return;
+      return new ErrorPage("invalid URL for policy file: " + target);
     }
 
     String rest = target.substring(secondSlash + 1);
     if (rest.contains("/") || !rest.endsWith(".gwt.rpc")) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      return;
+      return new ErrorPage("invalid name for policy file: " + rest);
     }
 
-    File policy = outboxes.findPolicyFile(rest);
-    if (policy == null) {
-      PageUtil.sendUnavailable(response, logger, "Policy file not found: " + rest);
-      return;
+    File fileToSend = outboxes.findPolicyFile(rest);
+    if (fileToSend == null) {
+      return new ErrorPage("Policy file not found: " + rest);
     }
 
-    PageUtil.sendFile("text/plain", policy, response);
-  }
-
-  private void sendJsonResult(JsonObject json, HttpServletRequest request,
-      HttpServletResponse response, TreeLogger logger) throws IOException {
-
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setHeader("Cache-control", "no-cache");
-    PrintWriter out = response.getWriter();
-
-    String callbackExpression = request.getParameter("_callback");
-    if (callbackExpression == null) {
-      // AJAX
-      response.setContentType("application/json");
-      json.write(out);
-    } else {
-      // JSONP
-      response.setContentType("application/javascript");
-      if (SAFE_CALLBACK.matcher(callbackExpression).matches()) {
-        out.print(callbackExpression + "(");
-        json.write(out);
-        out.println(");");
-      } else {
-        logger.log(TreeLogger.ERROR, "invalid callback: " + callbackExpression);
-        // Notice that we cannot execute the callback
-        out.print("alert('invalid callback parameter');\n");
-        json.write(out);
-      }
-    }
+    return Responses.newFileResponse("text/plain", fileToSend);
   }
 
   /**
    * Sends the log file as html with errors highlighted in red.
    */
-  private void sendLogPage(Outbox box, HttpServletResponse response)
+  private Response makeLogPage(final Outbox box)
        throws IOException {
-    File file = box.getCompileLog();
-    BufferedReader reader = new BufferedReader(new FileReader(file));
+    final File file = box.getCompileLog();
+    if (!file.isFile()) {
+      return new ErrorPage("log file not found");
+    }
 
-    response.setStatus(HttpServletResponse.SC_OK);
-    response.setContentType("text/html");
-    response.setHeader("Content-Style-Type", "text/css");
+    return new Response() {
 
-    HtmlWriter out = new HtmlWriter(response.getWriter());
-    out.startTag("html").nl();
-    out.startTag("head").nl();
-    out.startTag("title").text(box.getOutputModuleName() + " compile log").endTag("title").nl();
-    out.startTag("style").nl();
-    out.text(".error { color: red; font-weight: bold; }").nl();
-    out.endTag("style").nl();
-    out.endTag("head").nl();
-    out.startTag("body").nl();
-    sendLogAsHtml(reader, out);
-    out.endTag("body").nl();
-    out.endTag("html").nl();
+      @Override
+      public void send(HttpServletRequest request, HttpServletResponse response, TreeLogger logger)
+          throws IOException {
+        BufferedReader reader = new BufferedReader(new FileReader(file));
+
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("text/html");
+        response.setHeader("Content-Style-Type", "text/css");
+
+        HtmlWriter out = new HtmlWriter(response.getWriter());
+        out.startTag("html").nl();
+        out.startTag("head").nl();
+        out.startTag("title").text(box.getOutputModuleName() + " compile log").endTag("title").nl();
+        out.startTag("style").nl();
+        out.text(".error { color: red; font-weight: bold; }").nl();
+        out.endTag("style").nl();
+        out.endTag("head").nl();
+        out.startTag("body").nl();
+        sendLogAsHtml(reader, out);
+        out.endTag("body").nl();
+        out.endTag("html").nl();
+      }
+    };
   }
 
   private static final Pattern ERROR_PATTERN = Pattern.compile("\\[ERROR\\]");