com.google.gwt.dev.shell.GWTShellServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dev.shell.GWTShellServlet.java

Source

/*
 * Copyright 2008 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.shell;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.core.ext.linker.impl.HostedModeLinker;
import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
import com.google.gwt.dev.jjs.JJSOptionsImpl;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.dev.shell.log.ServletContextTreeLogger;
import com.google.gwt.dev.util.HttpHeaders;
import com.google.gwt.dev.util.Util;
import com.google.gwt.util.tools.Utility;

import org.apache.commons.collections.map.AbstractReferenceMap;
import org.apache.commons.collections.map.ReferenceIdentityMap;
import org.apache.commons.collections.map.ReferenceMap;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Built-in servlet for convenient access to the public path of a specified
 * module.
 */
public class GWTShellServlet extends HttpServlet {

    private static class RequestParts {
        public final String moduleName;

        public final String partialPath;

        public RequestParts(HttpServletRequest request) throws UnableToCompleteException {
            String pathInfo = request.getPathInfo();
            if (pathInfo != null) {
                int slash = pathInfo.indexOf('/', 1);
                if (slash != -1) {
                    moduleName = pathInfo.substring(1, slash);
                    partialPath = pathInfo.substring(slash + 1);
                    return;
                } else {
                    moduleName = pathInfo.substring(1);
                    partialPath = null;
                    return;
                }
            }
            throw new UnableToCompleteException();
        }
    }

    /**
     * This the default cache time in seconds for files that aren't either
     * *.cache.*, *.nocache.*.
     */
    private static final int DEFAULT_CACHE_SECONDS = 5;

    private static final String XHTML_MIME_TYPE = "application/xhtml+xml";

    /**
     * Must keep only weak references to ModuleDefs else we permanently pin them.
     */
    @SuppressWarnings("unchecked")
    private final Map<String, ModuleDef> loadedModulesByName = new ReferenceMap(AbstractReferenceMap.HARD,
            AbstractReferenceMap.WEAK);

    /**
     * The lifetime of the module pins the lifetime of the associated servlet;
     * this is because the loaded servlet has a weak backRef to its live module
     * through its context. When the module dies, the servlet needs to die also.
     */
    @SuppressWarnings("unchecked")
    private final Map<ModuleDef, Map<String, HttpServlet>> loadedServletsByModuleAndClassName = new ReferenceIdentityMap(
            AbstractReferenceMap.WEAK, AbstractReferenceMap.HARD, true);

    private final Map<String, String> mimeTypes = new HashMap<String, String>();

    /**
     * Only for backwards compatibility. Shouldn't we remove this now?
     */
    @SuppressWarnings("unchecked")
    private final Map<String, ModuleDef> modulesByServletPath = new ReferenceMap(AbstractReferenceMap.HARD,
            AbstractReferenceMap.WEAK);

    private int nextRequestId;

    private final Object requestIdLock = new Object();

    private TreeLogger topLogger;

    private WorkDirs workDirs;

    public GWTShellServlet() {
        initMimeTypes();
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processFileRequest(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processFileRequest(request, response);
    }

    protected void processFileRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {

        String pathInfo = request.getPathInfo();
        if (pathInfo.length() == 0 || pathInfo.equals("/")) {
            response.setContentType("text/html");
            PrintWriter writer = response.getWriter();
            writer.println("<html><body><basefont face='arial'>");
            writer.println(
                    "To launch an application, specify a URL of the form <code>/<i>module</i>/<i>file.html</i></code>");
            writer.println("</body></html>");
            return;
        }

        TreeLogger logger = getLogger();

        // Parse the request assuming it is module/resource.
        //
        RequestParts parts;
        try {
            parts = new RequestParts(request);
        } catch (UnableToCompleteException e) {
            sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
                    "Don't know what to do with this URL: '" + pathInfo + "'");
            return;
        }

        String partialPath = parts.partialPath;
        String moduleName = parts.moduleName;

        // If the module is renamed, substitute the renamed module name
        ModuleDef moduleDef = loadedModulesByName.get(moduleName);
        if (moduleDef != null) {
            moduleName = moduleDef.getName();
        }

        if (partialPath == null) {
            // Redir back to the same URL but ending with a slash.
            //
            response.sendRedirect(moduleName + "/");
            return;
        } else if (partialPath.length() > 0) {
            // Both the module name and a resource.
            //
            doGetPublicFile(request, response, logger, partialPath, moduleName);
            return;
        } else {
            // Was just the module name, ending with a slash.
            //
            doGetModule(request, response, logger, parts);
            return;
        }
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        TreeLogger logger = getLogger();
        int id = allocateRequestId();
        if (logger.isLoggable(TreeLogger.TRACE)) {
            StringBuffer url = request.getRequestURL();

            // Branch the logger in case we decide to log more below.
            logger = logger.branch(TreeLogger.TRACE, "Request " + id + ": " + url, null);
        }

        String servletClassName = null;
        ModuleDef moduleDef = null;

        try {
            // Attempt to split the URL into module/path, which we'll use to see
            // if we can map the request to a module's servlet.
            RequestParts parts = new RequestParts(request);

            if ("favicon.ico".equalsIgnoreCase(parts.moduleName)) {
                sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND, "Icon not available");
                return;
            }

            // See if the request references a module we know.
            moduleDef = getModuleDef(logger, parts.moduleName);
            if (moduleDef != null) {
                // Okay, we know this module. Do we know this servlet path?
                // It is right to prepend the slash because (1) ModuleDefSchema requires
                // every servlet path to begin with a slash and (2) RequestParts always
                // rips off the leading slash.
                String servletPath = "/" + parts.partialPath;
                servletClassName = moduleDef.findServletForPath(servletPath);

                // Fall-through below, where we check servletClassName.
            } else {
                // Fall-through below, where we check servletClassName.
            }
        } catch (UnableToCompleteException e) {
            // Do nothing, since it was speculative anyway.
        }

        // BEGIN BACKWARD COMPATIBILITY
        if (servletClassName == null) {
            // Try to map a bare path that isn't preceded by the module name.
            // This is no longer the recommended practice, so we warn.
            String path = request.getPathInfo();
            moduleDef = modulesByServletPath.get(path);
            if (moduleDef != null) {
                // See if there is a servlet we can delegate to for the given url.
                servletClassName = moduleDef.findServletForPath(path);

                if (servletClassName != null) {
                    TreeLogger branch = logger.branch(TreeLogger.WARN,
                            "Use of deprecated hosted mode servlet path mapping", null);
                    branch.log(TreeLogger.WARN,
                            "The client code is invoking the servlet with a URL that is not module-relative: "
                                    + path,
                            null);
                    branch.log(TreeLogger.WARN,
                            "Prepend GWT.getModuleBaseURL() to the URL in client code to create a module-relative URL: /"
                                    + moduleDef.getName() + path,
                            null);
                    branch.log(TreeLogger.WARN,
                            "Using module-relative URLs ensures correct URL-independent behavior in external servlet containers",
                            null);
                }

                // Fall-through below, where we check servletClassName.
            } else {
                // Fall-through below, where we check servletClassName.
            }
        }
        // END BACKWARD COMPATIBILITY

        // Load/get the servlet if we found one.
        if (servletClassName != null) {
            HttpServlet delegatee = tryGetOrLoadServlet(logger, moduleDef, servletClassName);
            if (delegatee == null) {
                logger.log(TreeLogger.ERROR, "Unable to dispatch request", null);
                sendErrorResponse(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                        "Unable to find/load mapped servlet class '" + servletClassName + "'");
                return;
            }

            // Delegate everything to the downstream servlet and we're done.
            delegatee.service(request, response);
        } else {
            // Use normal default processing on this request, since we couldn't
            // recognize it as anything special.
            super.service(request, response);
        }
    }

    private int allocateRequestId() {
        synchronized (requestIdLock) {
            return nextRequestId++;
        }
    }

    /**
     * Handle auto-generated resources.
     * 
     * @return <code>true</code> if a resource was generated
     */
    private boolean autoGenerateResources(HttpServletRequest request, HttpServletResponse response,
            TreeLogger logger, String partialPath, String moduleName) throws IOException {

        if (partialPath.equals(moduleName + ".nocache.js")) {
            if (request.getParameter("compiled") == null) {
                // Generate the .js file.
                try {
                    String js = genSelectionScript(logger, moduleName);
                    setResponseCacheHeaders(response, 0); // do not cache selection script
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.setContentType("text/javascript");
                    response.getWriter().println(js);
                    return true;
                } catch (UnableToCompleteException e) {
                    // The error will have already been logged. Continue, since this could
                    // actually be a request for a static file that happens to have an
                    // unfortunately confusing name.
                }
            }
        } else if (partialPath.equals("hosted.html")) {
            String html = HostedModeLinker.getHostedHtml();
            setResponseCacheHeaders(response, DEFAULT_CACHE_SECONDS);
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("text/html");
            response.getWriter().println(html);
            return true;
        }

        return false;
    }

    private void doGetModule(HttpServletRequest request, HttpServletResponse response, TreeLogger logger,
            RequestParts parts) throws IOException {

        // Generate a generic empty host page.
        //
        String msg = "The development shell servlet received a request to generate a host page for module '"
                + parts.moduleName + "' ";

        logger = logger.branch(TreeLogger.TRACE, msg, null);

        try {
            // Try to load the module just to make sure it'll work.
            getModuleDef(logger, parts.moduleName);
        } catch (UnableToCompleteException e) {
            sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND, "Unable to find/load module '"
                    + Util.escapeXml(parts.moduleName) + "' (see server log for details)");
            return;
        }

        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.println("<html><head>");
        writer.print("<script language='javascript' src='");
        writer.print(parts.moduleName);
        writer.println(".nocache.js'></script>");

        // Create a property for each query param.
        Map<String, String[]> params = getParameterMap(request);
        for (Map.Entry<String, String[]> entry : params.entrySet()) {
            String[] values = entry.getValue();
            if (values.length > 0) {
                writer.print("<meta name='gwt:property' content='");
                writer.print(entry.getKey());
                writer.print("=");
                writer.print(values[values.length - 1]);
                writer.println("'>");
            }
        }

        writer.println("</head><body>");
        writer.println("<iframe src=\"javascript:''\" id='__gwt_historyFrame' "
                + "style='position:absolute;width:0;height:0;border:0'></iframe>");
        writer.println("<noscript>");
        writer.println(
                "  <div style=\"width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif\">");
        writer.println("    Your web browser must have JavaScript enabled");
        writer.println("    in order for this application to display correctly.");
        writer.println("  </div>");
        writer.println("</noscript>");
        writer.println("</body></html>");

        // Done.
    }

    /**
     * Fetch a file and return it as the HTTP response, setting the cache-related
     * headers according to the name of the file (see
     * {@link #getCacheTime(String)}). This function honors If-Modified-Since to
     * minimize the impact of limiting caching of files for development.
     * 
     * @param request the HTTP request
     * @param response the HTTP response
     * @param logger a TreeLogger to use for debug output
     * @param partialPath the path within the module
     * @param moduleName the name of the module
     * @throws IOException
     */
    @SuppressWarnings("deprecation")
    private void doGetPublicFile(HttpServletRequest request, HttpServletResponse response, TreeLogger logger,
            String partialPath, String moduleName) throws IOException {

        // Create a logger branch for this request.
        logger = logger.branch(TreeLogger.TRACE, "The development shell servlet received a request for '"
                + partialPath + "' in module '" + moduleName + ".gwt.xml' ", null);

        // Handle auto-generation of resources.
        if (shouldAutoGenerateResources()) {
            if (autoGenerateResources(request, response, logger, partialPath, moduleName)) {
                return;
            }
        }

        URL foundResource = null;
        try {
            // Look for the requested file on the public path.
            //
            ModuleDef moduleDef = getModuleDef(logger, moduleName);
            if (shouldAutoGenerateResources()) {
                Resource publicResource = moduleDef.findPublicFile(partialPath);
                if (publicResource != null) {
                    foundResource = publicResource.getURL();
                }

                if (foundResource == null) {
                    // Look for public generated files
                    File shellDir = getShellWorkDirs().getShellPublicGenDir(moduleDef);
                    File requestedFile = new File(shellDir, partialPath);
                    if (requestedFile.exists()) {
                        try {
                            foundResource = requestedFile.toURI().toURL();
                        } catch (MalformedURLException e) {
                            // ignore since it was speculative anyway
                        }
                    }
                }
            }

            /*
             * If the user is coming from compiled web-mode, check the linker output
             * directory for the real bootstrap file.
             */
            if (foundResource == null) {
                File moduleDir = getShellWorkDirs().getCompilerOutputDir(moduleDef);
                File requestedFile = new File(moduleDir, partialPath);
                if (requestedFile.exists()) {
                    try {
                        foundResource = requestedFile.toURI().toURL();
                    } catch (MalformedURLException e) {
                        // ignore since it was speculative anyway
                    }
                }
            }

            if (foundResource == null) {
                String msg;
                if ("gwt.js".equals(partialPath)) {
                    msg = "Loading the old 'gwt.js' bootstrap script is no longer supported; please load '"
                            + moduleName + ".nocache.js' directly";
                } else {
                    msg = "Resource not found: " + partialPath + "; "
                            + "(could a file be missing from the public path or a <servlet> "
                            + "tag misconfigured in module " + moduleName + ".gwt.xml ?)";
                }
                logger.log(TreeLogger.WARN, msg, null);
                throw new UnableToCompleteException();
            }
        } catch (UnableToCompleteException e) {
            sendErrorResponse(response, HttpServletResponse.SC_NOT_FOUND,
                    "Cannot find resource '" + partialPath + "' in the public path of module '" + moduleName + "'");
            return;
        }

        // Get the MIME type.
        String path = foundResource.toExternalForm();
        String mimeType = null;
        try {
            mimeType = getServletContext().getMimeType(path);
        } catch (UnsupportedOperationException e) {
            // Certain minimalist servlet containers throw this.
            // Fall through to guess the type.
        }

        if (mimeType == null) {
            mimeType = guessMimeType(path);
            if (logger.isLoggable(TreeLogger.TRACE)) {
                logger.log(TreeLogger.TRACE, "Guessed MIME type '" + mimeType + "'", null);
            }
        }

        maybeIssueXhtmlWarning(logger, mimeType, partialPath);

        long cacheSeconds = getCacheTime(path);

        InputStream is = null;
        try {
            // Check for up-to-datedness.
            URLConnection conn = foundResource.openConnection();
            long lastModified = conn.getLastModified();
            if (isNotModified(request, lastModified)) {
                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                setResponseCacheHeaders(response, cacheSeconds);
                return;
            }

            // Set up headers to really send it.
            response.setStatus(HttpServletResponse.SC_OK);
            long now = new Date().getTime();
            response.setHeader(HttpHeaders.DATE, HttpHeaders.toInternetDateFormat(now));
            response.setContentType(mimeType);
            String lastModifiedStr = HttpHeaders.toInternetDateFormat(lastModified);
            response.setHeader(HttpHeaders.LAST_MODIFIED, lastModifiedStr);

            // Expiration header. Either immediately stale (requiring an
            // "If-Modified-Since") or infinitely cacheable (not requiring even a
            // freshness check).
            setResponseCacheHeaders(response, cacheSeconds);

            // Content length.
            int contentLength = conn.getContentLength();
            if (contentLength >= 0) {
                response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer.toString(contentLength));
            }

            // Send the bytes.
            is = conn.getInputStream();
            streamOut(is, response.getOutputStream(), 1024 * 8);
        } finally {
            Utility.close(is);
        }
    }

    /**
     * Generates a module.js file on the fly. Note that the nocache file that is
     * generated that can only be used for hosted mode. It cannot produce a web
     * mode version, since this servlet doesn't know strong names, since by
     * definition of "hosted mode" JavaScript hasn't been compiled at this point.
     */
    private String genSelectionScript(TreeLogger logger, String moduleName) throws UnableToCompleteException {
        if (logger.isLoggable(TreeLogger.TRACE)) {
            logger.log(TreeLogger.TRACE, "Generating a script selection script for module " + moduleName);
        }
        ModuleDef module = getModuleDef(logger, moduleName);
        StandardLinkerContext context = new StandardLinkerContext(logger, module, new JJSOptionsImpl());
        ArtifactSet artifacts = context.getArtifactsForPublicResources(logger, module);
        HostedModeLinker linker = new HostedModeLinker();
        return linker.generateSelectionScript(logger, context, artifacts);
    }

    /**
     * Get the length of time a given file should be cacheable. If the path
     * contains *.nocache.*, it is never cacheable; if it contains *.cache.*, it
     * is infinitely cacheable; anything else gets a default time.
     * 
     * @return cache time in seconds, or 0 if the file is not cacheable at all
     */
    private long getCacheTime(String path) {
        int lastDot = path.lastIndexOf('.');
        if (lastDot >= 0) {
            String prefix = path.substring(0, lastDot);
            if (prefix.endsWith(".cache")) {
                // RFC2616 says to never give a cache time of more than a year
                // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
                return HttpHeaders.SEC_YR;
            } else if (prefix.endsWith(".nocache")) {
                return 0;
            }
        }
        return DEFAULT_CACHE_SECONDS;
    }

    private synchronized TreeLogger getLogger() {
        if (topLogger == null) {
            ServletContext servletContext = getServletContext();
            final String attr = "com.google.gwt.dev.shell.logger";
            topLogger = (TreeLogger) servletContext.getAttribute(attr);
            if (topLogger == null) {
                // No shell available, so wrap the regular servlet context logger.
                //
                topLogger = new ServletContextTreeLogger(servletContext);
            }
        }
        return topLogger;
    }

    /**
     * We don't actually log this on purpose since the client does anyway.
     */
    private ModuleDef getModuleDef(TreeLogger logger, String moduleName) throws UnableToCompleteException {
        synchronized (loadedModulesByName) {
            ModuleDef moduleDef = loadedModulesByName.get(moduleName);
            if (moduleDef == null) {
                moduleDef = ModuleDefLoader.loadFromClassPath(logger, moduleName, false);
                loadedModulesByName.put(moduleName, moduleDef);
                loadedModulesByName.put(moduleDef.getName(), moduleDef);

                // BEGIN BACKWARD COMPATIBILITY
                // The following map of servlet path to module is included only
                // for backward-compatibility. We are going to remove this functionality
                // when we go out of beta. The new behavior is that the client should
                // specify the module name as part of the URL and construct it using
                // getModuleBaseURL().
                String[] servletPaths = moduleDef.getServletPaths();
                for (int i = 0; i < servletPaths.length; i++) {
                    modulesByServletPath.put(servletPaths[i], moduleDef);
                }
                // END BACKWARD COMPATIBILITY
            }
            return moduleDef;
        }
    }

    @SuppressWarnings("unchecked")
    private Map<String, String[]> getParameterMap(HttpServletRequest request) {
        return request.getParameterMap();
    }

    private synchronized WorkDirs getShellWorkDirs() {
        if (workDirs == null) {
            ServletContext servletContext = getServletContext();
            final String attr = "com.google.gwt.dev.shell.workdirs";
            workDirs = (WorkDirs) servletContext.getAttribute(attr);
            assert (workDirs != null);
        }
        return workDirs;
    }

    private String guessMimeType(String fullPath) {
        int dot = fullPath.lastIndexOf('.');
        if (dot != -1) {
            String ext = fullPath.substring(dot + 1);
            String mimeType = mimeTypes.get(ext);
            if (mimeType != null) {
                return mimeType;
            }

            // Otherwise, fall through.
            //
        }

        // Last resort.
        //
        return "application/octet-stream";
    }

    private void initMimeTypes() {
        mimeTypes.put("abs", "audio/x-mpeg");
        mimeTypes.put("ai", "application/postscript");
        mimeTypes.put("aif", "audio/x-aiff");
        mimeTypes.put("aifc", "audio/x-aiff");
        mimeTypes.put("aiff", "audio/x-aiff");
        mimeTypes.put("aim", "application/x-aim");
        mimeTypes.put("art", "image/x-jg");
        mimeTypes.put("asf", "video/x-ms-asf");
        mimeTypes.put("asx", "video/x-ms-asf");
        mimeTypes.put("au", "audio/basic");
        mimeTypes.put("avi", "video/x-msvideo");
        mimeTypes.put("avx", "video/x-rad-screenplay");
        mimeTypes.put("bcpio", "application/x-bcpio");
        mimeTypes.put("bin", "application/octet-stream");
        mimeTypes.put("bmp", "image/bmp");
        mimeTypes.put("body", "text/html");
        mimeTypes.put("cdf", "application/x-cdf");
        mimeTypes.put("cer", "application/x-x509-ca-cert");
        mimeTypes.put("class", "application/java");
        mimeTypes.put("cpio", "application/x-cpio");
        mimeTypes.put("csh", "application/x-csh");
        mimeTypes.put("css", "text/css");
        mimeTypes.put("dib", "image/bmp");
        mimeTypes.put("doc", "application/msword");
        mimeTypes.put("dtd", "text/plain");
        mimeTypes.put("dv", "video/x-dv");
        mimeTypes.put("dvi", "application/x-dvi");
        mimeTypes.put("eps", "application/postscript");
        mimeTypes.put("etx", "text/x-setext");
        mimeTypes.put("exe", "application/octet-stream");
        mimeTypes.put("gif", "image/gif");
        mimeTypes.put("gtar", "application/x-gtar");
        mimeTypes.put("gz", "application/x-gzip");
        mimeTypes.put("hdf", "application/x-hdf");
        mimeTypes.put("hqx", "application/mac-binhex40");
        mimeTypes.put("htc", "text/x-component");
        mimeTypes.put("htm", "text/html");
        mimeTypes.put("html", "text/html");
        mimeTypes.put("hqx", "application/mac-binhex40");
        mimeTypes.put("ief", "image/ief");
        mimeTypes.put("jad", "text/vnd.sun.j2me.app-descriptor");
        mimeTypes.put("jar", "application/java-archive");
        mimeTypes.put("java", "text/plain");
        mimeTypes.put("jnlp", "application/x-java-jnlp-file");
        mimeTypes.put("jpe", "image/jpeg");
        mimeTypes.put("jpeg", "image/jpeg");
        mimeTypes.put("jpg", "image/jpeg");
        mimeTypes.put("js", "text/javascript");
        mimeTypes.put("jsf", "text/plain");
        mimeTypes.put("jspf", "text/plain");
        mimeTypes.put("kar", "audio/x-midi");
        mimeTypes.put("latex", "application/x-latex");
        mimeTypes.put("m3u", "audio/x-mpegurl");
        mimeTypes.put("mac", "image/x-macpaint");
        mimeTypes.put("man", "application/x-troff-man");
        mimeTypes.put("me", "application/x-troff-me");
        mimeTypes.put("mid", "audio/x-midi");
        mimeTypes.put("midi", "audio/x-midi");
        mimeTypes.put("mif", "application/x-mif");
        mimeTypes.put("mov", "video/quicktime");
        mimeTypes.put("movie", "video/x-sgi-movie");
        mimeTypes.put("mp1", "audio/x-mpeg");
        mimeTypes.put("mp2", "audio/x-mpeg");
        mimeTypes.put("mp3", "audio/x-mpeg");
        mimeTypes.put("mpa", "audio/x-mpeg");
        mimeTypes.put("mpe", "video/mpeg");
        mimeTypes.put("mpeg", "video/mpeg");
        mimeTypes.put("mpega", "audio/x-mpeg");
        mimeTypes.put("mpg", "video/mpeg");
        mimeTypes.put("mpv2", "video/mpeg2");
        mimeTypes.put("ms", "application/x-wais-source");
        mimeTypes.put("nc", "application/x-netcdf");
        mimeTypes.put("oda", "application/oda");
        mimeTypes.put("pbm", "image/x-portable-bitmap");
        mimeTypes.put("pct", "image/pict");
        mimeTypes.put("pdf", "application/pdf");
        mimeTypes.put("pgm", "image/x-portable-graymap");
        mimeTypes.put("pic", "image/pict");
        mimeTypes.put("pict", "image/pict");
        mimeTypes.put("pls", "audio/x-scpls");
        mimeTypes.put("png", "image/png");
        mimeTypes.put("pnm", "image/x-portable-anymap");
        mimeTypes.put("pnt", "image/x-macpaint");
        mimeTypes.put("ppm", "image/x-portable-pixmap");
        mimeTypes.put("ppt", "application/powerpoint");
        mimeTypes.put("ps", "application/postscript");
        mimeTypes.put("psd", "image/x-photoshop");
        mimeTypes.put("qt", "video/quicktime");
        mimeTypes.put("qti", "image/x-quicktime");
        mimeTypes.put("qtif", "image/x-quicktime");
        mimeTypes.put("ras", "image/x-cmu-raster");
        mimeTypes.put("rgb", "image/x-rgb");
        mimeTypes.put("rm", "application/vnd.rn-realmedia");
        mimeTypes.put("roff", "application/x-troff");
        mimeTypes.put("rtf", "application/rtf");
        mimeTypes.put("rtx", "text/richtext");
        mimeTypes.put("sh", "application/x-sh");
        mimeTypes.put("shar", "application/x-shar");
        mimeTypes.put("smf", "audio/x-midi");
        mimeTypes.put("sit", "application/x-stuffit");
        mimeTypes.put("snd", "audio/basic");
        mimeTypes.put("src", "application/x-wais-source");
        mimeTypes.put("sv4cpio", "application/x-sv4cpio");
        mimeTypes.put("sv4crc", "application/x-sv4crc");
        mimeTypes.put("swf", "application/x-shockwave-flash");
        mimeTypes.put("t", "application/x-troff");
        mimeTypes.put("tar", "application/x-tar");
        mimeTypes.put("tcl", "application/x-tcl");
        mimeTypes.put("tex", "application/x-tex");
        mimeTypes.put("texi", "application/x-texinfo");
        mimeTypes.put("texinfo", "application/x-texinfo");
        mimeTypes.put("tif", "image/tiff");
        mimeTypes.put("tiff", "image/tiff");
        mimeTypes.put("tr", "application/x-troff");
        mimeTypes.put("tsv", "text/tab-separated-values");
        mimeTypes.put("txt", "text/plain");
        mimeTypes.put("ulw", "audio/basic");
        mimeTypes.put("ustar", "application/x-ustar");
        mimeTypes.put("xbm", "image/x-xbitmap");
        mimeTypes.put("xht", "application/xhtml+xml");
        mimeTypes.put("xhtml", "application/xhtml+xml");
        mimeTypes.put("xml", "text/xml");
        mimeTypes.put("xpm", "image/x-xpixmap");
        mimeTypes.put("xsl", "text/xml");
        mimeTypes.put("xwd", "image/x-xwindowdump");
        mimeTypes.put("wav", "audio/x-wav");
        mimeTypes.put("svg", "image/svg+xml");
        mimeTypes.put("svgz", "image/svg+xml");
        mimeTypes.put("vsd", "application/x-visio");
        mimeTypes.put("wbmp", "image/vnd.wap.wbmp");
        mimeTypes.put("wml", "text/vnd.wap.wml");
        mimeTypes.put("wmlc", "application/vnd.wap.wmlc");
        mimeTypes.put("wmls", "text/vnd.wap.wmlscript");
        mimeTypes.put("wmlscriptc", "application/vnd.wap.wmlscriptc");
        mimeTypes.put("wrl", "x-world/x-vrml");
        mimeTypes.put("Z", "application/x-compress");
        mimeTypes.put("z", "application/x-compress");
        mimeTypes.put("zip", "application/zip");
    }

    /**
     * Checks to see whether or not a client's file is out of date relative to the
     * original.
     */
    private boolean isNotModified(HttpServletRequest request, long ageOfServerCopy) {
        // The age of the server copy *must* have the milliseconds truncated.
        // Since milliseconds isn't part of the GMT format, failure to truncate
        // will leave the file in a state where it appears constantly out of date
        // and yet it can never get in sync because the Last-Modified date keeps
        // truncating off the milliseconds part on its way out.
        // 
        ageOfServerCopy -= (ageOfServerCopy % 1000);

        long ageOfClientCopy = 0;
        String ifModifiedSince = request.getHeader("If-Modified-Since");
        if (ifModifiedSince != null) {
            // Rip off any additional stuff at the end, such as "; length="
            // (IE does add this).
            //
            int lastSemi = ifModifiedSince.lastIndexOf(';');
            if (lastSemi != -1) {
                ifModifiedSince = ifModifiedSince.substring(0, lastSemi);
            }
            ageOfClientCopy = HttpHeaders.fromInternetDateFormat(ifModifiedSince);
        }

        if (ageOfClientCopy >= ageOfServerCopy) {
            // The client already has a good copy.
            //
            return true;
        } else {
            // The client needs a fresh copy of the requested file.
            //
            return false;
        }
    }

    private void maybeIssueXhtmlWarning(TreeLogger logger, String mimeType, String path) {
        if (!XHTML_MIME_TYPE.equals(mimeType)) {
            return;
        }

        String msg = "File was returned with content-type of \"" + mimeType
                + "\". GWT requires browser features that are not available to "
                + "documents with this content-type.";

        int ix = path.lastIndexOf('.');
        if (ix >= 0 && ix < path.length()) {
            String base = path.substring(0, ix);
            msg += " Consider renaming \"" + path + "\" to \"" + base + ".html\".";
        }

        logger.log(TreeLogger.WARN, msg, null);
    }

    private void sendErrorResponse(HttpServletResponse response, int statusCode, String msg) throws IOException {
        response.setContentType("text/html");
        response.getWriter().println(msg);
        response.setStatus(statusCode);
    }

    /**
     * Sets the Cache-control and Expires headers in the response based on the
     * supplied cache time.
     * 
     * Expires is used in addition to Cache-control for older clients or proxies
     * which may not properly understand Cache-control.
     * 
     * @param response the HttpServletResponse to update
     * @param cacheTime non-negative number of seconds to cache the response; 0
     *          means specifically do not allow caching at all.
     * @throws IllegalArgumentException if cacheTime is negative
     */
    private void setResponseCacheHeaders(HttpServletResponse response, long cacheTime) {
        long expires;
        if (cacheTime < 0) {
            throw new IllegalArgumentException("cacheTime of " + cacheTime + " is negative");
        }
        if (cacheTime > 0) {
            // Expire the specified seconds in the future.
            expires = new Date().getTime() + cacheTime * HttpHeaders.MS_SEC;
        } else {
            // Prevent caching by using a time in the past for cache expiration.
            // Use January 2, 1970 00:00:00, to account for timezone changes
            // in case a browser tries to convert to a local timezone first
            // 0=Jan 1, so add 1 day's worth of milliseconds to get Jan 2
            expires = HttpHeaders.SEC_DAY * HttpHeaders.MS_SEC;
        }
        response.setHeader(HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_MAXAGE + cacheTime);
        String expiresString = HttpHeaders.toInternetDateFormat(expires);
        response.setHeader(HttpHeaders.EXPIRES, expiresString);
    }

    private boolean shouldAutoGenerateResources() {
        ServletContext servletContext = getServletContext();
        final String attr = "com.google.gwt.dev.shell.shouldAutoGenerateResources";
        Boolean attrValue = (Boolean) servletContext.getAttribute(attr);
        if (attrValue == null) {
            return true;
        }
        return attrValue;
    }

    private void streamOut(InputStream in, OutputStream out, int bufferSize) throws IOException {
        assert (bufferSize >= 0);

        byte[] buffer = new byte[bufferSize];
        int bytesRead = 0;
        while (true) {
            bytesRead = in.read(buffer);
            if (bytesRead >= 0) {
                // Copy the bytes out.
                out.write(buffer, 0, bytesRead);
            } else {
                // End of input stream.
                out.flush();
                return;
            }
        }
    }

    private HttpServlet tryGetOrLoadServlet(TreeLogger logger, ModuleDef moduleDef, String className) {

        // Maps className to live servlet for this module.
        Map<String, HttpServlet> moduleServlets;
        synchronized (loadedServletsByModuleAndClassName) {
            moduleServlets = loadedServletsByModuleAndClassName.get(moduleDef);
            if (moduleServlets == null) {
                moduleServlets = new HashMap<String, HttpServlet>();
                loadedServletsByModuleAndClassName.put(moduleDef, moduleServlets);
            }
        }

        synchronized (moduleServlets) {
            HttpServlet servlet = moduleServlets.get(className);
            if (servlet != null) {
                // Found it.
                //
                return servlet;
            }

            // Try to load and instantiate it.
            //
            Throwable caught = null;
            try {
                Class<?> servletClass = Class.forName(className);
                Object newInstance = servletClass.newInstance();
                if (!(newInstance instanceof HttpServlet)) {
                    logger.log(TreeLogger.ERROR, "Not compatible with HttpServlet: " + className
                            + " (does your service extend RemoteServiceServlet?)", null);
                    return null;
                }

                // Success. Hang onto the instance so we can reuse it.
                //
                servlet = (HttpServlet) newInstance;

                // We create proxies for ServletContext and ServletConfig to enable
                // RemoteServiceServlets to load public and generated resources via
                // ServletContext.getResourceAsStream()
                //
                ServletContext context = new HostedModeServletContextProxy(getServletContext(), moduleDef,
                        getShellWorkDirs());
                ServletConfig config = new HostedModeServletConfigProxy(getServletConfig(), context);

                servlet.init(config);

                moduleServlets.put(className, servlet);
                return servlet;
            } catch (ClassNotFoundException e) {
                caught = e;
            } catch (InstantiationException e) {
                caught = e;
            } catch (IllegalAccessException e) {
                caught = e;
            } catch (ServletException e) {
                caught = e;
            }
            String msg = "Unable to instantiate '" + className + "'";
            logger.log(TreeLogger.ERROR, msg, caught);
            return null;
        }
    }
}