blob: cc3b07b29d8b60e6f736b1811bc32d7906875d49 [file] [log] [blame]
/*
* Copyright 2011 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 java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Serves Java source files so that a browser's debugger can display them.
* (This works with browsers that follow the <a
* href="https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit"
* >Source Map Spec</a>, such as Chrome.)
*
* <p>The debugger will first fetch the source map from
* /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>
*/
class SourceHandler {
/**
* The URL prefix for all source maps and Java source code.
*/
static final String SOURCEMAP_PATH = "/sourcemaps/";
/**
* The suffix that Super Dev Mode uses in source map URL's.
*/
private static final String SOURCEMAP_URL_SUFFIX = "_sourcemap.json";
/**
* Matches a valid source map json file request.
*
* Used to extract the strong name of the permutation:
* StrongName_sourceMap0.json
*/
private static final Pattern SOURCEMAP_FILENAME_PATTERN = Pattern.compile(
"^(" + WebServer.STRONG_NAME + ")" + Pattern.quote(SOURCEMAP_URL_SUFFIX) + "$");
/**
* Matches a valid source map request.
*
* Used to extract the module name:
* /sourcemaps/ModuleName/.....
*/
private static final Pattern SOURCEMAP_MODULE_PATTERN = Pattern.compile(
"^" + SOURCEMAP_PATH + "([^/]+)/");
static final String SOURCEROOT_TEMPLATE_VARIABLE = "$sourceroot_goes_here$";
private final OutboxTable outboxTable;
private final JsonExporter exporter;
SourceHandler(OutboxTable outboxTable, JsonExporter exporter) {
this.outboxTable = outboxTable;
this.exporter = exporter;
}
static boolean isSourceMapRequest(String target) {
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;
}
Response handle(String target, HttpServletRequest request, TreeLogger logger)
throws IOException {
String moduleName = getModuleNameFromRequest(target);
if (moduleName == null) {
throw new RuntimeException("invalid request (shouldn't happen): " + target);
}
Outbox box = outboxTable.findByOutputModuleName(moduleName);
if (box == null) {
return new ErrorPage("No such module: " + moduleName);
} else if (box.containsStubCompile()) {
return new ErrorPage("This module hasn't been compiled yet.");
}
String rootDir = SOURCEMAP_PATH + moduleName + "/";
String rest = target.substring(rootDir.length());
if (rest.isEmpty()) {
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.)
return makeSourceMapPage(moduleName, box.findSourceMapForOnePermutation(), request);
} else if (rest.endsWith("/")) {
return sendFileListPage(box, rest);
} else if (rest.endsWith(".java")) {
return makeSourcePage(box, rest, request.getQueryString(), logger);
} else {
String strongName = getStrongNameFromSourcemapFilename(rest);
if (strongName != null) {
File sourceMap = box.findSourceMap(strongName).getAbsoluteFile();
return makeSourceMapPage(moduleName, sourceMap, request);
} else {
return new ErrorPage("page not found");
}
}
}
static String getModuleNameFromRequest(String target) {
Matcher matcher = SOURCEMAP_MODULE_PATTERN.matcher(target);
return matcher.find() ? matcher.group(1) : null;
}
static String getStrongNameFromSourcemapFilename(String target) {
Matcher matcher = SOURCEMAP_FILENAME_PATTERN.matcher(target);
return matcher.matches() ? matcher.group(1) : null;
}
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.)
// We need to do this at runtime because we don't know what the hostname will be
// until we get a request. (For example, some people run the Code Server behind
// a reverse proxy to support https.)
String sourceRoot = String.format("http://%s:%d/sourcemaps/%s/", request.getServerName(),
request.getServerPort(), moduleName);
final Response barePage = Responses.newTextTemplateResponse("application/json", sourceMap,
"\"" + SOURCEROOT_TEMPLATE_VARIABLE + "\"",
"\"" + sourceRoot + "\"");
// Wrap it in another response to time how long it takes.
return Responses.newTimedResponse(barePage, "sent source map for module '" + moduleName + "'");
}
private Response makeDirectoryListPage(Outbox box) throws IOException {
SourceMap map = SourceMap.load(box.findSourceMapForOnePermutation());
JsonObject json = exporter.exportSourceMapDirectoryListVars(box, map);
return Pages.newHtmlPage("config", json, "directorylist.html");
}
private Response sendFileListPage(Outbox box, String rest) throws IOException {
SourceMap map = SourceMap.load(box.findSourceMapForOnePermutation());
JsonObject json = exporter.exportSourceMapFileListVars(box, map, rest);
return Pages.newHtmlPage("config", json, "filelist.html");
}
/**
* 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 Response makeSourcePage(Outbox box, String sourcePath, String query, TreeLogger logger)
throws IOException {
InputStream pageBytes = box.openSourceFile(sourcePath);
if (pageBytes == null) {
return new ErrorPage("unknown source file: " + sourcePath);
}
if (query != null && query.equals("html")) {
return makeHtmlSourcePage(box, sourcePath, pageBytes, logger);
} else {
return Responses.newBinaryStreamResponse("text/plain", pageBytes);
}
}
/**
* 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 Response makeHtmlSourcePage(Outbox box, final String sourcePath,
final InputStream pageBytes, TreeLogger logger) throws IOException {
final ReverseSourceMap sourceMap = ReverseSourceMap.load(logger,
box.findSourceMapForOnePermutation());
final File sourceFile = new File(sourcePath);
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();
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();
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();
}
out.endTag("pre").nl();
out.endTag("body").nl();
out.endTag("html").nl();
}
};
}
}