Super Dev Mode: better errors when we haven't compiled yet

These pages can't work without compiler output:
  - sourcemaps and source code listings
  - compile error log

Also changed the module page to not link to them.

Refactoring: pass an Outbox instead of a module name
in more places.

Change-Id: I3a8b3dcf7744fa164cc670d0938b946aa86c1403
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/HtmlWriter.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/HtmlWriter.java
index 91fa680..f2949b9 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/HtmlWriter.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/HtmlWriter.java
@@ -29,7 +29,7 @@
   private static final Set<String> ALLOWED_TAGS =
       Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
           "html", "head", "title", "style",
-          "body", "h1", "h2", "h3", "h4", "h5", "h6", "a", "pre", "span",
+          "body", "h1", "h2", "h3", "h4", "h5", "h6", "p", "a", "pre", "span",
           "table", "tr", "td")));
   private static final Set<String> ALLOWED_ATTS =
       Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java
index 5f645c8..d61ca7f 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/JsonExporter.java
@@ -83,6 +83,7 @@
     JsonObject result = new JsonObject();
     result.put("moduleName", box.getOutputModuleName()); // TODO: rename
     result.put("files", exportOutputFiles(box));
+    result.put("isCompiled", !box.containsStubCompile());
     return result;
   }
 
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java
index 0a84805..4531395 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Outbox.java
@@ -150,6 +150,14 @@
   }
 
   /**
+   * Returns true if we haven't done a real compile yet, so the Outbox contains
+   * a stub that will automatically start a compile.
+   */
+  synchronized boolean containsStubCompile() {
+    return publishedJob == null;
+  }
+
+  /**
    * Returns the module name that will be sent to the compiler (before renaming).
    */
   String getInputModuleName() {
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java
index 54ce117..220a487 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/OutboxTable.java
@@ -18,9 +18,11 @@
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 import com.google.gwt.thirdparty.guava.common.collect.Maps;
 
+import java.io.File;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -44,6 +46,10 @@
     outboxes.put(outbox.getId(), outbox);
   }
 
+  ImmutableList<Outbox> getOutboxes() {
+    return ImmutableList.copyOf(outboxes.values());
+  }
+
   /**
    * Retrieves an {@link Outbox} corresponding to a given module name.
    * This should be the module name after renaming.
@@ -74,4 +80,19 @@
       box.maybePrecompile(logger);
     }
   }
+
+  /**
+   * Given the name of a policy file, searches all the boxes for a file with that name.
+   * Returns null if not found.
+   */
+  File findPolicyFile(String filename) {
+    for (Outbox box : outboxes.values()) {
+      File candidate = box.getOutputFile(box.getOutputModuleName() + "/" + filename);
+      if (candidate.isFile()) {
+        return candidate;
+      }
+    }
+    // not found
+    return null;
+  }
 }
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
index c22a127..9a92d03 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/PageUtil.java
@@ -17,6 +17,7 @@
 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;
@@ -220,4 +221,28 @@
     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/SourceHandler.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
index 8ac9bfb..16e1057 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
@@ -103,8 +103,10 @@
 
     Outbox box = outboxes.findByOutputModuleName(moduleName);
     if (box == null) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      logger.log(TreeLogger.WARN, "unknown module; returned not found for request: " + target);
+      PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
+      return;
+    } else if (box.containsStubCompile()) {
+      PageUtil.sendUnavailable(response, logger, "This module hasn't been compiled yet.");
       return;
     }
 
@@ -112,14 +114,14 @@
     String rest = target.substring(rootDir.length());
 
     if (rest.isEmpty()) {
-      sendDirectoryListPage(moduleName, response, logger);
+      sendDirectoryListPage(box, response, logger);
     } 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);
     } else if (rest.endsWith("/")) {
-      sendFileListPage(moduleName, rest, response, logger);
+      sendFileListPage(box, rest, response, logger);
     } else if (rest.endsWith(".java")) {
       sendSourceFile(box, rest, request.getQueryString(), response, logger);
     } else {
@@ -169,19 +171,17 @@
         "' in " + elapsedTime + " ms");
   }
 
-  private void sendDirectoryListPage(String moduleName, HttpServletResponse response,
+  private void sendDirectoryListPage(Outbox box, HttpServletResponse response,
       TreeLogger logger) throws IOException {
 
-    Outbox box = outboxes.findByOutputModuleName(moduleName);
     SourceMap map = SourceMap.load(box.findSourceMapForOnePermutation());
     JsonObject json = exporter.exportSourceMapDirectoryListVars(box, map);
     PageUtil.sendJsonAndHtml("config", json, "directorylist.html", response, logger);
   }
 
-  private void sendFileListPage(String moduleName, String rest, HttpServletResponse response,
+  private void sendFileListPage(Outbox box, String rest, HttpServletResponse response,
       TreeLogger logger) throws IOException {
 
-    Outbox box = outboxes.findByOutputModuleName(moduleName);
     SourceMap map = SourceMap.load(box.findSourceMapForOnePermutation());
     JsonObject json = exporter.exportSourceMapFileListVars(box, map, rest);
     PageUtil.sendJsonAndHtml("config", json, "filelist.html", response, logger);
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 d5a1e74..979e13f 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -200,10 +200,9 @@
     if (target.startsWith("/recompile/")) {
       setHandled(request);
       String moduleName = target.substring("/recompile/".length());
-      Outbox outbox = outboxes.findByOutputModuleName(moduleName);
-      if (outbox == null) {
-        response.sendError(HttpServletResponse.SC_NOT_FOUND);
-        logger.log(TreeLogger.WARN, "not found: " + target);
+      Outbox box = outboxes.findByOutputModuleName(moduleName);
+      if (box == null) {
+        PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
         return;
       }
 
@@ -213,7 +212,7 @@
       // cause a spurious recompile, resulting in an unexpected permutation being loaded later.
       //
       // It would be unsafe to allow a configuration property to be changed.
-      Job job = outbox.makeJob(getBindingProperties(request), logger);
+      Job job = box.makeJob(getBindingProperties(request), logger);
       runner.submit(job);
       Job.Result result = job.waitForResult();
       JsonObject json = jsonExporter.exportRecompileResponse(result);
@@ -224,8 +223,14 @@
     if (target.startsWith("/log/")) {
       setHandled(request);
       String moduleName = target.substring("/log/".length());
-      File file = outboxes.findByOutputModuleName(moduleName).getCompileLog();
-      sendLogPage(moduleName, file, response);
+      Outbox box = outboxes.findByOutputModuleName(moduleName);
+      if (box == null) {
+        PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
+      } else if (box.containsStubCompile()) {
+        PageUtil.sendUnavailable(response, logger, "This module hasn't been compiled yet.");
+      } else {
+        sendLogPage(box, response);
+      }
       return;
     }
 
@@ -299,12 +304,16 @@
 
     int secondSlash = target.indexOf('/', 1);
     String moduleName = target.substring(1, secondSlash);
-    Outbox outbox = outboxes.findByOutputModuleName(moduleName);
+    Outbox box = outboxes.findByOutputModuleName(moduleName);
+    if (box == null) {
+      PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
+      return;
+    }
 
-    File file = outbox.getOutputFile(target);
+    File file = box.getOutputFile(target);
     if (!file.isFile()) {
       // perhaps it's compressed
-      file = outbox.getOutputFile(target + ".gz");
+      file = box.getOutputFile(target + ".gz");
       if (!file.isFile()) {
         response.sendError(HttpServletResponse.SC_NOT_FOUND);
         logger.log(TreeLogger.WARN, "not found: " + file.toString());
@@ -334,10 +343,10 @@
       throws IOException {
     Outbox box = outboxes.findByOutputModuleName(moduleName);
     if (box == null) {
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      logger.log(TreeLogger.WARN, "module not found: " + moduleName);
+      PageUtil.sendUnavailable(response, logger, "No such module: " + moduleName);
       return;
     }
+
     JsonObject json = jsonExporter.exportModulePageVars(box);
     PageUtil.sendJsonAndHtml("config", json, "modulepage.html", response, logger);
   }
@@ -356,11 +365,10 @@
 
     out.startTag("h1").text("Policy Files").endTag("h1").nl();
 
-    for (String moduleName : outboxes.getOutputModuleNames()) {
-      Outbox module = outboxes.findByOutputModuleName(moduleName);
-      File manifest = module.getExtraFile("rpcPolicyManifest/manifest.txt");
+    for (Outbox box : outboxes.getOutboxes()) {
+      File manifest = box.getExtraFile("rpcPolicyManifest/manifest.txt");
       if (manifest.isFile()) {
-        out.startTag("h2").text(moduleName).endTag("h2").nl();
+        out.startTag("h2").text(box.getOutputModuleName()).endTag("h2").nl();
 
         out.startTag("table").nl();
         String text = PageUtil.loadFile(manifest);
@@ -377,7 +385,7 @@
           String serviceName = fields[0];
           String policyFileName = fields[1];
 
-          String serviceUrl = SourceHandler.SOURCEMAP_PATH + moduleName + "/" +
+          String serviceUrl = SourceHandler.SOURCEMAP_PATH + box.getOutputModuleName() + "/" +
               serviceName.replace('.', '/') + ".java";
           String policyUrl = "/policies/" + policyFileName;
 
@@ -403,28 +411,26 @@
 
   private void sendPolicyFile(String target, HttpServletResponse response, TreeLogger logger)
       throws IOException {
+
     int secondSlash = target.indexOf('/', 1);
     if (secondSlash < 1) {
       response.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
     }
+
     String rest = target.substring(secondSlash + 1);
     if (rest.contains("/") || !rest.endsWith(".gwt.rpc")) {
       response.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
     }
 
-    for (String moduleName : outboxes.getOutputModuleNames()) {
-      Outbox module = outboxes.findByOutputModuleName(moduleName);
-      File policy = module.getOutputFile(moduleName + "/" + rest);
-      if (policy.isFile()) {
-        PageUtil.sendFile("text/plain", policy, response);
-        return;
-      }
+    File policy = outboxes.findPolicyFile(rest);
+    if (policy == null) {
+      PageUtil.sendUnavailable(response, logger, "Policy file not found: " + rest);
+      return;
     }
 
-    logger.log(TreeLogger.Type.WARN, "policy file not found: " + rest);
-    response.sendError(HttpServletResponse.SC_NOT_FOUND);
+    PageUtil.sendFile("text/plain", policy, response);
   }
 
   private void sendJsonResult(JsonObject json, HttpServletRequest request,
@@ -458,8 +464,9 @@
   /**
    * Sends the log file as html with errors highlighted in red.
    */
-  private void sendLogPage(String moduleName, File file, HttpServletResponse response)
+  private void sendLogPage(Outbox box, HttpServletResponse response)
        throws IOException {
+    File file = box.getCompileLog();
     BufferedReader reader = new BufferedReader(new FileReader(file));
 
     response.setStatus(HttpServletResponse.SC_OK);
@@ -469,7 +476,7 @@
     HtmlWriter out = new HtmlWriter(response.getWriter());
     out.startTag("html").nl();
     out.startTag("head").nl();
-    out.startTag("title").text(moduleName + " compile log").endTag("title").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();
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/modulepage.html b/dev/codeserver/java/com/google/gwt/dev/codeserver/modulepage.html
index 29a8647..1a67396 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/modulepage.html
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/modulepage.html
@@ -21,13 +21,26 @@
       document.title = title;
       setTextContent(document.getElementById("title"), title);
 
-      document.getElementById("logLink")
-              .setAttribute("href", "../log/" + config.moduleName);
-      document.getElementById("srcLink")
-              .setAttribute("href", "../sourcemaps/" + config.moduleName + "/");
+      if (config.isCompiled) {
+          clearClassName(".show-if-compiled");
+          document.getElementById("logLink")
+                  .setAttribute("href", "../log/" + config.moduleName);
+          document.getElementById("srcLink")
+                  .setAttribute("href", "../sourcemaps/" + config.moduleName + "/");
+      } else {
+          clearClassName(".show-if-not-compiled");
+      }
+
       updateFileList(config, document.getElementById("files"));
     }
 
+    function clearClassName(query) {
+        var elts = document.querySelectorAll(query);
+        for (var i = 0; i < elts.length; i++) {
+            elts[i].className = "";
+        }
+    }
+
     function updateFileList(config, resultElement) {
       for (var i = 0; i < config.files.length; i++) {
         var file = config.files[i];
@@ -63,12 +76,23 @@
     }
   </script>
 
+  <style type="text/css">
+    .show-if-compiled, .show-if-not-compiled {
+        display: none;
+    }
+  </style>
 </head>
 <body>
 <h1 id="title">Loading...</h1>
 
+<div class="show-if-not-compiled">
+<p>This module hasn't been compiled yet.</p>
+</div>
+
+<div class="show-if-compiled">
 <p><a id="logLink">Messages</a> from the last time this module was compiled.</p>
 <p><a id="srcLink">Source</a> from the last time this module was compiled.</p>
+</div>
 
 <h2>Output Files</h2>
 <p>(From the last successful compile.)</p>