Super Dev Mode: sourcemap URL fixes

Externally visible changes:
- put back old URL. (Needed for some Google integration tests.)
- changed sourcemap filename in URL's to [strong name]_sourcemap.json
  (The '0' is not needed and it doesn't need to match the filename on
 disk.)
- fixed a bug in a regular expression: The period in ".json" was not
  escaped so it would match anything.

Refactoring:
- keep the code for URL-matching separate from the code for finding
sourcemap files on disk.
- Fixed the unit tests to report better errors. Inlined some constants
for readability.

Change-Id: I366b9b13b0026d922428ae3f1ae56abad9d07059
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/ModuleState.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/ModuleState.java
index 9f5db6d..e107df4 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/ModuleState.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/ModuleState.java
@@ -38,6 +38,12 @@
  * to recompile it and where the compiler output is.
  */
 class ModuleState {
+
+  /**
+   * The suffix that the GWT compiler uses when writing a sourcemap file.
+   */
+  private static final String SOURCEMAP_FILE_SUFFIX = "_sourceMap0.json";
+
   private final AtomicReference<CompileDir> current = new AtomicReference<CompileDir>();
   private final Recompiler recompiler;
   private final TreeLogger logger;
@@ -93,16 +99,18 @@
   }
 
   /**
-   * Returns the source map file from the most recent recompile.
+   * Returns the source map file from the most recent recompile,
+   * assuming there is one permutation.
+   *
    * @throws RuntimeException if unable
    */
-  File findSourceMap() {
+  File findSourceMapForOnePermutation() {
     File dir = findSymbolMapDir();
 
     File[] sourceMapFiles = dir.listFiles(new FilenameFilter() {
       @Override
       public boolean accept(File dir, String name) {
-        return name.matches(".*" + SourceHandler.SOURCEMAP_SUFFIX);
+        return name.endsWith(SOURCEMAP_FILE_SUFFIX);
       }
     });
 
@@ -122,10 +130,24 @@
   }
 
   /**
+   * Returns the source map file given a strong name.
+   *
+   * @throws RuntimeException if unable
+   */
+  public File findSourceMap(String strongName) {
+    File dir = findSymbolMapDir();
+    File file = new File(dir, strongName + SOURCEMAP_FILE_SUFFIX);
+    if (!file.isFile()) {
+      throw new RuntimeException("Sourcemap file doesn't exist for " + strongName);
+    }
+    return file;
+  }
+
+  /**
    * Returns the symbols map folder for this modulename.
    * @throws RuntimeException if unable
    */
-  File findSymbolMapDir() {
+  private File findSymbolMapDir() {
     String moduleName = recompiler.getModuleName();
     File symbolMapsDir = current.get().findSymbolMapDir(moduleName);
     if (symbolMapsDir == null) {
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 cd1fe49..1c42f2c 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/Recompiler.java
@@ -360,7 +360,7 @@
     // Fix bug with SDM and Chrome 24+ where //@ sourceURL directives cause X-SourceMap header to be ignored
     // Frustratingly, Chrome won't canonicalize a relative URL
     overrideConfig(moduleDef, "includeSourceMapUrl", "http://" + serverPrefix +
-        WebServer.sourceMapLocationForModule(moduleDef.getName()));
+        SourceHandler.sourceMapLocationTemplate(moduleDef.getName()));
 
     // If present, set some config properties back to defaults.
     // (Needed for Google's server-side linker.)
diff --git a/dev/codeserver/java/com/google/gwt/dev/codeserver/ReverseSourceMap.java b/dev/codeserver/java/com/google/gwt/dev/codeserver/ReverseSourceMap.java
index a000d71..2b81781 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/ReverseSourceMap.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/ReverseSourceMap.java
@@ -37,7 +37,7 @@
    */
   static ReverseSourceMap load(TreeLogger logger, ModuleState moduleState) {
     SourceMapConsumerV3 consumer = new SourceMapConsumerV3();
-    String unparsed = Util.readFileAsString(moduleState.findSourceMap());
+    String unparsed = Util.readFileAsString(moduleState.findSourceMapForOnePermutation());
     try {
       consumer.parse(unparsed);
       return new ReverseSourceMap(consumer);
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 53b9d14..2982d5e 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/SourceHandler.java
@@ -38,9 +38,9 @@
  * >Source Map Spec</a>, such as Chrome.)
  *
  * <p>The debugger will first fetch the source map from
- * /sourcemaps/\{module name\}/gwtSourceMap.json. This file contains the names of Java
+ * /sourcemaps/[module name]/[strong name]_sourcemap.json. This file contains the names of Java
  * source files to download. Each source file will have a path like
- * "/sourcemaps/\{module name\}/src/{filename}".</p>
+ * "/sourcemaps/[module name]/src/[filename]".</p>
  */
 class SourceHandler {
 
@@ -50,9 +50,9 @@
   static final String SOURCEMAP_PATH = "/sourcemaps/";
 
   /**
-   * The suffix of a source map location json file.
+   * The suffix that Super Dev Mode uses in source map URL's.
    */
-  static final String SOURCEMAP_SUFFIX = "_sourceMap0.json";
+  private static final String SOURCEMAP_URL_SUFFIX = "_sourcemap.json";
 
   /**
    * Matches a valid source map json file request.
@@ -61,7 +61,7 @@
    *   StrongName_sourceMap0.json
    */
   private static final Pattern SOURCEMAP_FILENAME_PATTERN = Pattern.compile(
-      "^([\\dA-F]{32})" + SOURCEMAP_SUFFIX + "$");
+      "^(" + WebServer.STRONG_NAME + ")" + Pattern.quote(SOURCEMAP_URL_SUFFIX) + "$");
 
   /**
    * Matches a valid source map request.
@@ -87,6 +87,14 @@
     return getModuleNameFromRequest(target) != null;
   }
 
+  /**
+   * The template for the sourcemap location to give the compiler.
+   * It contains one template variable, __HASH__ for the strong name.
+   */
+  static String sourceMapLocationTemplate(String moduleName) {
+    return SOURCEMAP_PATH + moduleName + "/__HASH__" + SOURCEMAP_URL_SUFFIX;
+  }
+
   void handle(String target, HttpServletRequest request, HttpServletResponse response)
       throws IOException {
     String moduleName = getModuleNameFromRequest(target);
@@ -99,6 +107,12 @@
 
     if (rest.isEmpty()) {
       sendDirectoryListPage(moduleName, response);
+    } 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.)
+      ModuleState moduleState = modules.get(moduleName);
+      sendSourceMap(moduleName, moduleState.findSourceMapForOnePermutation(), request, response);
     } else if (rest.endsWith("/")) {
       sendFileListPage(moduleName, rest, response);
     } else if (rest.endsWith(".java")) {
@@ -106,7 +120,9 @@
     } else {
       String strongName = getStrongNameFromSourcemapFilename(rest);
       if (strongName != null) {
-        sendSourceMap(moduleName, strongName, request, response);
+        ModuleState moduleState = modules.get(moduleName);
+        File sourceMap = moduleState.findSourceMap(strongName).getAbsoluteFile();
+        sendSourceMap(moduleName, sourceMap, request, response);
       } else {
         response.sendError(HttpServletResponse.SC_NOT_FOUND);
         logger.log(TreeLogger.WARN, "returned not found for request: " + target);
@@ -124,17 +140,11 @@
     return matcher.matches() ? matcher.group(1) : null;
   }
 
-  private void sendSourceMap(String moduleName, String strongName, HttpServletRequest request,
+  private void sendSourceMap(String moduleName, File sourceMap, HttpServletRequest request,
       HttpServletResponse response) throws IOException {
 
     long startTime = System.currentTimeMillis();
 
-    ModuleState moduleState = modules.get(moduleName);
-
-    String sourceMapPath = moduleState.findSymbolMapDir().getAbsolutePath();
-
-    File sourceMap = new File(sourceMapPath + "/" + strongName + SOURCEMAP_SUFFIX);
-
     // Stream the file, substituting the sourceroot variable with the filename.
     // (This is more efficient than parsing the file as JSON.)
 
@@ -269,6 +279,6 @@
 
   private SourceMap loadSourceMap(String moduleName) {
     ModuleState moduleState = modules.get(moduleName);
-    return SourceMap.load(moduleState.findSourceMap());
+    return SourceMap.load(moduleState.findSourceMapForOnePermutation());
   }
 }
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 66fe315..7fcc85c 100644
--- a/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
+++ b/dev/codeserver/java/com/google/gwt/dev/codeserver/WebServer.java
@@ -82,6 +82,10 @@
   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 final SourceHandler handler;
@@ -296,9 +300,12 @@
       response.setHeader("Content-Encoding", "gzip");
     }
 
-    if (target.endsWith(".cache.js")) {
-      String strongName = target.replaceFirst("^.*/(.+).cache.js$", "$1");
-      response.setHeader("X-SourceMap", sourceMapLocationForModule(moduleName, strongName));
+    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);
     }
     response.setHeader("Access-Control-Allow-Origin", "*");
     String mimeType = guessMimeType(target);
@@ -504,15 +511,6 @@
     return result;
   }
 
-  static String sourceMapLocationForModule(String moduleName) {
-    return sourceMapLocationForModule(moduleName, "__HASH__");
-  }
-
-  private static String sourceMapLocationForModule(String moduleName, String strongName) {
-    return SourceHandler.SOURCEMAP_PATH + moduleName + "/" + strongName
-        + SourceHandler.SOURCEMAP_SUFFIX;
-  }
-
   private static void setHandled(HttpServletRequest request) {
     Request baseRequest = (request instanceof Request) ? (Request) request :
         AbstractHttpConnection.getCurrentConnection().getRequest();
diff --git a/dev/codeserver/test/com/google/gwt/dev/codeserver/SourceHandlerTest.java b/dev/codeserver/test/com/google/gwt/dev/codeserver/SourceHandlerTest.java
index 1451b45..c1a89a4 100644
--- a/dev/codeserver/test/com/google/gwt/dev/codeserver/SourceHandlerTest.java
+++ b/dev/codeserver/test/com/google/gwt/dev/codeserver/SourceHandlerTest.java
@@ -1,8 +1,5 @@
 package com.google.gwt.dev.codeserver;
 
-import static com.google.gwt.dev.codeserver.SourceHandler.SOURCEMAP_PATH;
-import static com.google.gwt.dev.codeserver.SourceHandler.SOURCEMAP_SUFFIX;
-
 import com.google.gwt.dev.util.Util;
 
 import static org.junit.Assert.assertEquals;
@@ -17,7 +14,6 @@
  */
 public class SourceHandlerTest {
 
-  private static final String VALID_MODULE_NAME = "myModule";
   private static final String VALID_STRONG_NAME = Util.computeStrongName("foo-bar".getBytes());
 
   /**
@@ -25,17 +21,14 @@
    */
   @Test
   public void testIsSourceMapRequest() {
-    assertTrue(SourceHandler.isSourceMapRequest(SOURCEMAP_PATH + VALID_MODULE_NAME + "/"));
-    assertTrue(SourceHandler.isSourceMapRequest(SOURCEMAP_PATH + VALID_MODULE_NAME + "/whatever"));
-    assertTrue(SourceHandler.isSourceMapRequest(SOURCEMAP_PATH + VALID_MODULE_NAME + "/folder/"));
-    assertTrue(SourceHandler.isSourceMapRequest(
-        SOURCEMAP_PATH + VALID_MODULE_NAME + "/folder/file.ext"));
-    assertTrue(SourceHandler.isSourceMapRequest(
-        SOURCEMAP_PATH + VALID_MODULE_NAME + "/" + VALID_STRONG_NAME + SOURCEMAP_SUFFIX));
+    checkSourceMapRequest("/sourcemaps/myModule/");
+    checkSourceMapRequest("/sourcemaps/myModule/whatever");
+    checkSourceMapRequest("/sourcemaps/myModule/folder/");
+    checkSourceMapRequest("/sourcemaps/myModule/folder/file.ext");
+    checkSourceMapRequest("/sourcemaps/myModule/" + VALID_STRONG_NAME + "_sourcemap.json");
 
-    assertFalse(SourceHandler.isSourceMapRequest(SOURCEMAP_PATH + VALID_MODULE_NAME));
-    assertFalse(SourceHandler.isSourceMapRequest(
-        "whatever" + SOURCEMAP_PATH + VALID_MODULE_NAME + "/"));
+    checkNotSourceMapRequest("/sourcemaps/myModule");
+    checkNotSourceMapRequest("whatever/sourcemaps/myModule/");
   }
 
   /**
@@ -43,21 +36,36 @@
    */
   @Test
   public void testGetModuleNameFromRequest() {
-    assertEquals(VALID_MODULE_NAME, SourceHandler.getModuleNameFromRequest(
-        SOURCEMAP_PATH + VALID_MODULE_NAME + "/"));
-    assertEquals(VALID_MODULE_NAME, SourceHandler.getModuleNameFromRequest(
-        SOURCEMAP_PATH + VALID_MODULE_NAME + "/" + VALID_STRONG_NAME + SOURCEMAP_SUFFIX));
+    assertEquals("myModule", SourceHandler.getModuleNameFromRequest(
+        "/sourcemaps/myModule/"));
+    assertEquals("myModule", SourceHandler.getModuleNameFromRequest(
+        "/sourcemaps/myModule/1234_sourcemap.json"));
   }
 
   /**
    * Test {@link SourceHandler#getStrongNameFromSourcemapFilename(String)}
    */
   @Test
-  public void testGwtStrongNameFromSourcemapFilename() {
+  public void testGetStrongNameFromSourcemapFilename() {
     assertEquals(VALID_STRONG_NAME, SourceHandler
-        .getStrongNameFromSourcemapFilename(VALID_STRONG_NAME + SOURCEMAP_SUFFIX));
-    assertNull(SourceHandler.getStrongNameFromSourcemapFilename("invalid_hash" + SOURCEMAP_SUFFIX));
-    assertNull(SourceHandler.getStrongNameFromSourcemapFilename(
-        "whatever/" + VALID_STRONG_NAME + SOURCEMAP_SUFFIX));
+        .getStrongNameFromSourcemapFilename(VALID_STRONG_NAME + "_sourcemap.json"));
+    checkNoStrongName("invalid_hash_sourcemap.json");
+    checkNoStrongName("whatever/" + VALID_STRONG_NAME + "_sourcemap.json");
+    checkNoStrongName(VALID_STRONG_NAME + "_sourcemap/json");
+  }
+
+  private void checkSourceMapRequest(String validUrl) {
+    assertTrue("should be a valid sourcemap URL but isn't: " + validUrl,
+      SourceHandler.isSourceMapRequest(validUrl));
+  }
+
+  private void checkNotSourceMapRequest(String validUrl) {
+    assertFalse("should not be a valid sourcemap URL but is: " + validUrl,
+      SourceHandler.isSourceMapRequest(validUrl));
+  }
+
+  private void checkNoStrongName(String rest) {
+    assertNull("shouldn't have returned a strong name for: " + rest,
+      SourceHandler.getStrongNameFromSourcemapFilename(rest));
   }
 }