001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.shell;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.core.ext.UnableToCompleteException;
020: import com.google.gwt.dev.cfg.ModuleDef;
021: import com.google.gwt.dev.cfg.ModuleDefLoader;
022: import com.google.gwt.dev.util.HttpHeaders;
023: import com.google.gwt.dev.util.SelectionScriptGenerator;
024: import com.google.gwt.dev.util.Util;
025: import com.google.gwt.dev.util.log.ServletContextTreeLogger;
026:
027: import java.io.File;
028: import java.io.IOException;
029: import java.io.InputStream;
030: import java.io.OutputStream;
031: import java.io.PrintWriter;
032: import java.net.MalformedURLException;
033: import java.net.URL;
034: import java.net.URLConnection;
035: import java.util.Date;
036: import java.util.HashMap;
037: import java.util.Map;
038:
039: import javax.servlet.ServletConfig;
040: import javax.servlet.ServletContext;
041: import javax.servlet.ServletException;
042: import javax.servlet.http.HttpServlet;
043: import javax.servlet.http.HttpServletRequest;
044: import javax.servlet.http.HttpServletResponse;
045:
046: /**
047: * Built-in servlet for convenient access to the public path of a specified
048: * module.
049: */
050: public class GWTShellServlet extends HttpServlet {
051:
052: private static class RequestParts {
053: public final String moduleName;
054:
055: public final String partialPath;
056:
057: public RequestParts(HttpServletRequest request)
058: throws UnableToCompleteException {
059: String pathInfo = request.getPathInfo();
060: if (pathInfo != null) {
061: int slash = pathInfo.indexOf('/', 1);
062: if (slash != -1) {
063: moduleName = pathInfo.substring(1, slash);
064: partialPath = pathInfo.substring(slash + 1);
065: return;
066: } else {
067: moduleName = pathInfo.substring(1);
068: partialPath = null;
069: return;
070: }
071: }
072: throw new UnableToCompleteException();
073: }
074: }
075:
076: /**
077: * This the default cache time in seconds for files that aren't either
078: * *.cache.*, *.nocache.*.
079: */
080: private static final int DEFAULT_CACHE_SECONDS = 5;
081:
082: private static final String XHTML_MIME_TYPE = "application/xhtml+xml";
083:
084: private final Map<String, ModuleDef> loadedModulesByName = new HashMap<String, ModuleDef>();
085:
086: private final Map<String, HttpServlet> loadedServletsByModuleAndClassName = new HashMap<String, HttpServlet>();
087:
088: private final Map<String, String> mimeTypes = new HashMap<String, String>();
089:
090: private final Map<String, ModuleDef> modulesByServletPath = new HashMap<String, ModuleDef>();
091:
092: private int nextRequestId;
093:
094: private File outDir;
095:
096: private final Object requestIdLock = new Object();
097:
098: private TreeLogger topLogger;
099:
100: public GWTShellServlet() {
101: initMimeTypes();
102: }
103:
104: @Override
105: protected void doGet(HttpServletRequest request,
106: HttpServletResponse response) throws ServletException,
107: IOException {
108: processFileRequest(request, response);
109: }
110:
111: @Override
112: protected void doPost(HttpServletRequest request,
113: HttpServletResponse response) throws ServletException,
114: IOException {
115: processFileRequest(request, response);
116: }
117:
118: protected void processFileRequest(HttpServletRequest request,
119: HttpServletResponse response) throws IOException {
120:
121: String pathInfo = request.getPathInfo();
122: if (pathInfo.length() == 0 || pathInfo.equals("/")) {
123: response.setContentType("text/html");
124: PrintWriter writer = response.getWriter();
125: writer.println("<html><body><basefont face='arial'>");
126: writer
127: .println("To launch an application, specify a URL of the form <code>/<i>module</i>/<i>file.html</i></code>");
128: writer.println("</body></html>");
129: return;
130: }
131:
132: TreeLogger logger = getLogger();
133:
134: // Parse the request assuming it is module/resource.
135: //
136: RequestParts parts;
137: try {
138: parts = new RequestParts(request);
139: } catch (UnableToCompleteException e) {
140: sendErrorResponse(response,
141: HttpServletResponse.SC_NOT_FOUND,
142: "Don't know what to do with this URL: '" + pathInfo
143: + "'");
144: return;
145: }
146:
147: String partialPath = parts.partialPath;
148: String moduleName = parts.moduleName;
149:
150: if (partialPath == null) {
151: // Redir back to the same URL but ending with a slash.
152: //
153: response.sendRedirect(moduleName + "/");
154: return;
155: } else if (partialPath.length() > 0) {
156: // Both the module name and a resource.
157: //
158: doGetPublicFile(request, response, logger, partialPath,
159: moduleName);
160: return;
161: } else {
162: // Was just the module name, ending with a slash.
163: //
164: doGetModule(request, response, logger, parts);
165: return;
166: }
167: }
168:
169: @Override
170: protected void service(HttpServletRequest request,
171: HttpServletResponse response) throws ServletException,
172: IOException {
173:
174: TreeLogger logger = getLogger();
175: int id = allocateRequestId();
176: if (logger.isLoggable(TreeLogger.TRACE)) {
177: StringBuffer url = request.getRequestURL();
178:
179: // Branch the logger in case we decide to log more below.
180: logger = logger.branch(TreeLogger.TRACE, "Request " + id
181: + ": " + url, null);
182: }
183:
184: String servletClassName = null;
185: ModuleDef moduleDef = null;
186:
187: try {
188: // Attempt to split the URL into module/path, which we'll use to see
189: // if we can map the request to a module's servlet.
190: RequestParts parts = new RequestParts(request);
191:
192: // See if the request references a module we know.
193: // Note that we do *not* actually try to load the module here, because
194: // we're only looking for servlet invocations, which can only happen
195: // when we have *already* loaded the destination module to serve up the
196: // client code in the first place.
197: moduleDef = loadedModulesByName.get(parts.moduleName);
198: if (moduleDef != null) {
199: // Okay, we know this module. Do we know this servlet path?
200: // It is right to prepend the slash because (1) ModuleDefSchema requires
201: // every servlet path to begin with a slash and (2) RequestParts always
202: // rips off the leading slash.
203: String servletPath = "/" + parts.partialPath;
204: servletClassName = moduleDef
205: .findServletForPath(servletPath);
206:
207: // Fall-through below, where we check servletClassName.
208: } else {
209: // Fall-through below, where we check servletClassName.
210: }
211: } catch (UnableToCompleteException e) {
212: // Do nothing, since it was speculative anyway.
213: }
214:
215: // BEGIN BACKWARD COMPATIBILITY
216: if (servletClassName == null) {
217: // Try to map a bare path that isn't preceded by the module name.
218: // This is no longer the recommended practice, so we warn.
219: String path = request.getPathInfo();
220: moduleDef = modulesByServletPath.get(path);
221: if (moduleDef != null) {
222: // See if there is a servlet we can delegate to for the given url.
223: servletClassName = moduleDef.findServletForPath(path);
224:
225: if (servletClassName != null) {
226: TreeLogger branch = logger
227: .branch(
228: TreeLogger.WARN,
229: "Use of deprecated hosted mode servlet path mapping",
230: null);
231: branch
232: .log(
233: TreeLogger.WARN,
234: "The client code is invoking the servlet with a URL that is not module-relative: "
235: + path, null);
236: branch
237: .log(
238: TreeLogger.WARN,
239: "Prepend GWT.getModuleBaseURL() to the URL in client code to create a module-relative URL: /"
240: + moduleDef.getName()
241: + path, null);
242: branch
243: .log(
244: TreeLogger.WARN,
245: "Using module-relative URLs ensures correct URL-independent behavior in external servlet containers",
246: null);
247: }
248:
249: // Fall-through below, where we check servletClassName.
250: } else {
251: // Fall-through below, where we check servletClassName.
252: }
253: }
254: // END BACKWARD COMPATIBILITY
255:
256: // Load/get the servlet if we found one.
257: if (servletClassName != null) {
258: HttpServlet delegatee = tryGetOrLoadServlet(logger,
259: moduleDef, servletClassName);
260: if (delegatee == null) {
261: logger.log(TreeLogger.ERROR,
262: "Unable to dispatch request", null);
263: sendErrorResponse(response,
264: HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
265: "Unable to find/load mapped servlet class '"
266: + servletClassName + "'");
267: return;
268: }
269:
270: // Delegate everything to the downstream servlet and we're done.
271: delegatee.service(request, response);
272: } else {
273: // Use normal default processing on this request, since we couldn't
274: // recognize it as anything special.
275: super .service(request, response);
276: }
277: }
278:
279: private int allocateRequestId() {
280: synchronized (requestIdLock) {
281: return nextRequestId++;
282: }
283: }
284:
285: /**
286: * Handle auto-generated resources.
287: *
288: * @return <code>true</code> if a resource was generated
289: */
290: private boolean autoGenerateResources(HttpServletRequest request,
291: HttpServletResponse response, TreeLogger logger,
292: String partialPath, String moduleName) throws IOException {
293:
294: // If the request is of the form ".../moduleName.nocache.js" or
295: // ".../moduleName-xs.nocache.js" then generate the selection script for
296: // them.
297: boolean nocacheHtml = partialPath.equals(moduleName
298: + ".nocache.js");
299: boolean nocacheScript = !nocacheHtml
300: && partialPath.equals(moduleName + "-xs.nocache.js");
301: if (nocacheHtml || nocacheScript) {
302: // If the '?compiled' request property is specified, don't auto-generate.
303: if (request.getParameter("compiled") == null) {
304: // Generate the .js file.
305: try {
306: String js = genSelectionScript(logger, moduleName,
307: nocacheScript);
308: setResponseCacheHeaders(response, 0); // do not cache selection script
309: response.setStatus(HttpServletResponse.SC_OK);
310: response.setContentType("text/javascript");
311: response.getWriter().println(js);
312: return true;
313: } catch (UnableToCompleteException e) {
314: // The error will have already been logged. Continue, since this could
315: // actually be a request for a static file that happens to have an
316: // unfortunately confusing name.
317: }
318: }
319: }
320:
321: return false;
322: }
323:
324: private void doGetModule(HttpServletRequest request,
325: HttpServletResponse response, TreeLogger logger,
326: RequestParts parts) throws IOException {
327:
328: if ("favicon.ico".equalsIgnoreCase(parts.moduleName)) {
329: sendErrorResponse(response,
330: HttpServletResponse.SC_NOT_FOUND,
331: "Icon not available");
332: return;
333: }
334:
335: // Generate a generic empty host page.
336: //
337: String msg = "The development shell servlet received a request to generate a host page for module '"
338: + parts.moduleName + "' ";
339:
340: logger = logger.branch(TreeLogger.TRACE, msg, null);
341:
342: try {
343: // Try to load the module just to make sure it'll work.
344: getModuleDef(logger, parts.moduleName);
345: } catch (UnableToCompleteException e) {
346: sendErrorResponse(response,
347: HttpServletResponse.SC_NOT_FOUND,
348: "Unable to find/load module '"
349: + Util.escapeXml(parts.moduleName)
350: + "' (see server log for details)");
351: return;
352: }
353:
354: response.setContentType("text/html");
355: PrintWriter writer = response.getWriter();
356: writer.println("<html><head>");
357: writer.print("<script language='javascript' src='");
358: writer.print(parts.moduleName);
359: writer.println(".nocache.js'></script>");
360:
361: // Create a property for each query param.
362: Map<String, String[]> params = request.getParameterMap();
363: for (Map.Entry<String, String[]> entry : params.entrySet()) {
364: String[] values = entry.getValue();
365: if (values.length > 0) {
366: writer.print("<meta name='gwt:property' content='");
367: writer.print(entry.getKey());
368: writer.print("=");
369: writer.print(values[values.length - 1]);
370: writer.println("'>");
371: }
372: }
373:
374: writer.println("</head><body>");
375: writer
376: .println("<iframe src=\"javascript:''\" id='__gwt_historyFrame' "
377: + "style='position:absolute;width:0;height:0;border:0'></iframe>");
378: writer.println("</body></html>");
379:
380: // Done.
381: }
382:
383: /**
384: * Fetch a file and return it as the HTTP response, setting the cache-related
385: * headers according to the name of the file (see
386: * {@link #getCacheTime(String)}). This function honors If-Modified-Since to
387: * minimize the impact of limiting caching of files for development.
388: *
389: * @param request the HTTP request
390: * @param response the HTTP response
391: * @param logger a TreeLogger to use for debug output
392: * @param partialPath the path within the module
393: * @param moduleName the name of the module
394: * @throws IOException
395: */
396: private void doGetPublicFile(HttpServletRequest request,
397: HttpServletResponse response, TreeLogger logger,
398: String partialPath, String moduleName) throws IOException {
399:
400: // Create a logger branch for this request.
401: String msg = "The development shell servlet received a request for '"
402: + partialPath
403: + "' in module '"
404: + moduleName
405: + ".gwt.xml' ";
406: logger = logger.branch(TreeLogger.TRACE, msg, null);
407:
408: // Handle auto-generation of resources.
409: if (autoGenerateResources(request, response, logger,
410: partialPath, moduleName)) {
411: return;
412: }
413:
414: URL foundResource;
415: try {
416: // Look for the requested file on the public path.
417: //
418: ModuleDef moduleDef = getModuleDef(logger, moduleName);
419: foundResource = moduleDef.findPublicFile(partialPath);
420:
421: if (foundResource == null) {
422: // Look in the place where we write compiled output.
423: File moduleDir = new File(getOutputDir(), moduleName);
424: File requestedFile = new File(moduleDir, partialPath);
425: if (requestedFile.exists()) {
426: try {
427: foundResource = requestedFile.toURI().toURL();
428: } catch (MalformedURLException e) {
429: // ignore since it was speculative anyway
430: }
431: }
432:
433: if (foundResource == null) {
434: msg = "Resource not found: "
435: + partialPath
436: + "\n"
437: + "(Could a file be missing from the public path or a <servlet> "
438: + "tag misconfigured in module "
439: + moduleName + ".gwt.xml ?)";
440: logger.log(TreeLogger.WARN, msg, null);
441: throw new UnableToCompleteException();
442: }
443: }
444: } catch (UnableToCompleteException e) {
445: sendErrorResponse(response,
446: HttpServletResponse.SC_NOT_FOUND,
447: "Cannot find resource '" + partialPath
448: + "' in the public path of module '"
449: + moduleName + "'");
450: return;
451: }
452:
453: // Get the MIME type.
454: String path = foundResource.toExternalForm();
455: String mimeType = null;
456: try {
457: mimeType = getServletContext().getMimeType(path);
458: } catch (UnsupportedOperationException e) {
459: // Certain minimalist servlet containers throw this.
460: // Fall through to guess the type.
461: }
462:
463: if (mimeType == null) {
464: mimeType = guessMimeType(path);
465: msg = "Guessed MIME type '" + mimeType + "'";
466: logger.log(TreeLogger.TRACE, msg, null);
467: }
468:
469: maybeIssueXhtmlWarning(logger, mimeType, partialPath);
470:
471: long cacheSeconds = getCacheTime(path);
472:
473: InputStream is = null;
474: try {
475: // Check for up-to-datedness.
476: URLConnection conn = foundResource.openConnection();
477: long lastModified = conn.getLastModified();
478: if (isNotModified(request, lastModified)) {
479: response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
480: setResponseCacheHeaders(response, cacheSeconds);
481: return;
482: }
483:
484: // Set up headers to really send it.
485: response.setStatus(HttpServletResponse.SC_OK);
486: long now = new Date().getTime();
487: response.setHeader(HttpHeaders.DATE, HttpHeaders
488: .toInternetDateFormat(now));
489: response.setContentType(mimeType);
490: String lastModifiedStr = HttpHeaders
491: .toInternetDateFormat(lastModified);
492: response.setHeader(HttpHeaders.LAST_MODIFIED,
493: lastModifiedStr);
494:
495: // Expiration header. Either immediately stale (requiring an
496: // "If-Modified-Since") or infinitely cacheable (not requiring even a
497: // freshness check).
498: setResponseCacheHeaders(response, cacheSeconds);
499:
500: // Content length.
501: int contentLength = conn.getContentLength();
502: if (contentLength >= 0) {
503: response.setHeader(HttpHeaders.CONTENT_LENGTH, Integer
504: .toString(contentLength));
505: }
506:
507: // Send the bytes.
508: is = foundResource.openStream();
509: streamOut(is, response.getOutputStream(), 1024 * 8);
510: } finally {
511: if (is != null) {
512: try {
513: is.close();
514: } catch (IOException swallowed) {
515: // Nothing we can do now.
516: //
517: }
518: }
519: }
520: }
521:
522: /**
523: * Generates a module.js file on the fly. Note that the nocache file that is
524: * generated that can only be used for hosted mode. It cannot produce a web
525: * mode version, since this servlet doesn't know strong names, since by
526: * definition of "hosted mode" JavaScript hasn't been compiled at this point.
527: */
528: private String genSelectionScript(TreeLogger logger,
529: String moduleName, boolean asScript)
530: throws UnableToCompleteException {
531: String msg = asScript ? "Generating a script selection script for module "
532: : "Generating an html selection script for module ";
533: msg += moduleName;
534: logger.log(TreeLogger.TRACE, msg, null);
535:
536: ModuleDef moduleDef = getModuleDef(logger, moduleName);
537: SelectionScriptGenerator gen = new SelectionScriptGenerator(
538: moduleDef);
539: return gen.generateSelectionScript(false, asScript);
540: }
541:
542: /**
543: * Get the length of time a given file should be cacheable. If the path
544: * contains *.nocache.*, it is never cacheable; if it contains *.cache.*, it
545: * is infinitely cacheable; anything else gets a default time.
546: *
547: * @return cache time in seconds, or 0 if the file is not cacheable at all
548: */
549: private long getCacheTime(String path) {
550: int lastDot = path.lastIndexOf('.');
551: if (lastDot >= 0) {
552: String prefix = path.substring(0, lastDot);
553: if (prefix.endsWith(".cache")) {
554: // RFC2616 says to never give a cache time of more than a year
555: // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21
556: return HttpHeaders.SEC_YR;
557: } else if (prefix.endsWith(".nocache")) {
558: return 0;
559: }
560: }
561: return DEFAULT_CACHE_SECONDS;
562: }
563:
564: private synchronized TreeLogger getLogger() {
565: if (topLogger == null) {
566: ServletContext servletContext = getServletContext();
567: final String attr = "com.google.gwt.dev.shell.logger";
568: topLogger = (TreeLogger) servletContext.getAttribute(attr);
569: if (topLogger == null) {
570: // No shell available, so wrap the regular servlet context logger.
571: //
572: topLogger = new ServletContextTreeLogger(servletContext);
573: }
574: }
575: return topLogger;
576: }
577:
578: /**
579: * We don't actually log this on purpose since the client does anyway.
580: */
581: private ModuleDef getModuleDef(TreeLogger logger, String moduleName)
582: throws UnableToCompleteException {
583: synchronized (loadedModulesByName) {
584: ModuleDef moduleDef = loadedModulesByName.get(moduleName);
585: if (moduleDef == null) {
586: moduleDef = ModuleDefLoader.loadFromClassPath(logger,
587: moduleName);
588: loadedModulesByName.put(moduleName, moduleDef);
589:
590: // BEGIN BACKWARD COMPATIBILITY
591: // The following map of servlet path to module is included only
592: // for backward-compatibility. We are going to remove this functionality
593: // when we go out of beta. The new behavior is that the client should
594: // specify the module name as part of the URL and construct it using
595: // getModuleBaseURL().
596: String[] servletPaths = moduleDef.getServletPaths();
597: for (int i = 0; i < servletPaths.length; i++) {
598: ModuleDef oldDef = modulesByServletPath.put(
599: servletPaths[i], moduleDef);
600: if (oldDef != null) {
601: logger.log(TreeLogger.WARN,
602: "Undefined behavior: Servlet path "
603: + servletPaths[i]
604: + " conflicts in modules "
605: + moduleDef.getName() + " and "
606: + oldDef.getName(), null);
607: }
608: }
609: // END BACKWARD COMPATIBILITY
610: }
611: return moduleDef;
612: }
613: }
614:
615: private synchronized File getOutputDir() {
616: if (outDir == null) {
617: ServletContext servletContext = getServletContext();
618: final String attr = "com.google.gwt.dev.shell.outdir";
619: outDir = (File) servletContext.getAttribute(attr);
620: assert (outDir != null);
621: }
622: return outDir;
623: }
624:
625: private String guessMimeType(String fullPath) {
626: int dot = fullPath.lastIndexOf('.');
627: if (dot != -1) {
628: String ext = fullPath.substring(dot + 1);
629: String mimeType = mimeTypes.get(ext);
630: if (mimeType != null) {
631: return mimeType;
632: }
633:
634: // Otherwise, fall through.
635: //
636: }
637:
638: // Last resort.
639: //
640: return "application/octet-stream";
641: }
642:
643: private void initMimeTypes() {
644: mimeTypes.put("abs", "audio/x-mpeg");
645: mimeTypes.put("ai", "application/postscript");
646: mimeTypes.put("aif", "audio/x-aiff");
647: mimeTypes.put("aifc", "audio/x-aiff");
648: mimeTypes.put("aiff", "audio/x-aiff");
649: mimeTypes.put("aim", "application/x-aim");
650: mimeTypes.put("art", "image/x-jg");
651: mimeTypes.put("asf", "video/x-ms-asf");
652: mimeTypes.put("asx", "video/x-ms-asf");
653: mimeTypes.put("au", "audio/basic");
654: mimeTypes.put("avi", "video/x-msvideo");
655: mimeTypes.put("avx", "video/x-rad-screenplay");
656: mimeTypes.put("bcpio", "application/x-bcpio");
657: mimeTypes.put("bin", "application/octet-stream");
658: mimeTypes.put("bmp", "image/bmp");
659: mimeTypes.put("body", "text/html");
660: mimeTypes.put("cdf", "application/x-cdf");
661: mimeTypes.put("cer", "application/x-x509-ca-cert");
662: mimeTypes.put("class", "application/java");
663: mimeTypes.put("cpio", "application/x-cpio");
664: mimeTypes.put("csh", "application/x-csh");
665: mimeTypes.put("css", "text/css");
666: mimeTypes.put("dib", "image/bmp");
667: mimeTypes.put("doc", "application/msword");
668: mimeTypes.put("dtd", "text/plain");
669: mimeTypes.put("dv", "video/x-dv");
670: mimeTypes.put("dvi", "application/x-dvi");
671: mimeTypes.put("eps", "application/postscript");
672: mimeTypes.put("etx", "text/x-setext");
673: mimeTypes.put("exe", "application/octet-stream");
674: mimeTypes.put("gif", "image/gif");
675: mimeTypes.put("gtar", "application/x-gtar");
676: mimeTypes.put("gz", "application/x-gzip");
677: mimeTypes.put("hdf", "application/x-hdf");
678: mimeTypes.put("hqx", "application/mac-binhex40");
679: mimeTypes.put("htc", "text/x-component");
680: mimeTypes.put("htm", "text/html");
681: mimeTypes.put("html", "text/html");
682: mimeTypes.put("hqx", "application/mac-binhex40");
683: mimeTypes.put("ief", "image/ief");
684: mimeTypes.put("jad", "text/vnd.sun.j2me.app-descriptor");
685: mimeTypes.put("jar", "application/java-archive");
686: mimeTypes.put("java", "text/plain");
687: mimeTypes.put("jnlp", "application/x-java-jnlp-file");
688: mimeTypes.put("jpe", "image/jpeg");
689: mimeTypes.put("jpeg", "image/jpeg");
690: mimeTypes.put("jpg", "image/jpeg");
691: mimeTypes.put("js", "text/javascript");
692: mimeTypes.put("jsf", "text/plain");
693: mimeTypes.put("jspf", "text/plain");
694: mimeTypes.put("kar", "audio/x-midi");
695: mimeTypes.put("latex", "application/x-latex");
696: mimeTypes.put("m3u", "audio/x-mpegurl");
697: mimeTypes.put("mac", "image/x-macpaint");
698: mimeTypes.put("man", "application/x-troff-man");
699: mimeTypes.put("me", "application/x-troff-me");
700: mimeTypes.put("mid", "audio/x-midi");
701: mimeTypes.put("midi", "audio/x-midi");
702: mimeTypes.put("mif", "application/x-mif");
703: mimeTypes.put("mov", "video/quicktime");
704: mimeTypes.put("movie", "video/x-sgi-movie");
705: mimeTypes.put("mp1", "audio/x-mpeg");
706: mimeTypes.put("mp2", "audio/x-mpeg");
707: mimeTypes.put("mp3", "audio/x-mpeg");
708: mimeTypes.put("mpa", "audio/x-mpeg");
709: mimeTypes.put("mpe", "video/mpeg");
710: mimeTypes.put("mpeg", "video/mpeg");
711: mimeTypes.put("mpega", "audio/x-mpeg");
712: mimeTypes.put("mpg", "video/mpeg");
713: mimeTypes.put("mpv2", "video/mpeg2");
714: mimeTypes.put("ms", "application/x-wais-source");
715: mimeTypes.put("nc", "application/x-netcdf");
716: mimeTypes.put("oda", "application/oda");
717: mimeTypes.put("pbm", "image/x-portable-bitmap");
718: mimeTypes.put("pct", "image/pict");
719: mimeTypes.put("pdf", "application/pdf");
720: mimeTypes.put("pgm", "image/x-portable-graymap");
721: mimeTypes.put("pic", "image/pict");
722: mimeTypes.put("pict", "image/pict");
723: mimeTypes.put("pls", "audio/x-scpls");
724: mimeTypes.put("png", "image/png");
725: mimeTypes.put("pnm", "image/x-portable-anymap");
726: mimeTypes.put("pnt", "image/x-macpaint");
727: mimeTypes.put("ppm", "image/x-portable-pixmap");
728: mimeTypes.put("ppt", "application/powerpoint");
729: mimeTypes.put("ps", "application/postscript");
730: mimeTypes.put("psd", "image/x-photoshop");
731: mimeTypes.put("qt", "video/quicktime");
732: mimeTypes.put("qti", "image/x-quicktime");
733: mimeTypes.put("qtif", "image/x-quicktime");
734: mimeTypes.put("ras", "image/x-cmu-raster");
735: mimeTypes.put("rgb", "image/x-rgb");
736: mimeTypes.put("rm", "application/vnd.rn-realmedia");
737: mimeTypes.put("roff", "application/x-troff");
738: mimeTypes.put("rtf", "application/rtf");
739: mimeTypes.put("rtx", "text/richtext");
740: mimeTypes.put("sh", "application/x-sh");
741: mimeTypes.put("shar", "application/x-shar");
742: mimeTypes.put("smf", "audio/x-midi");
743: mimeTypes.put("sit", "application/x-stuffit");
744: mimeTypes.put("snd", "audio/basic");
745: mimeTypes.put("src", "application/x-wais-source");
746: mimeTypes.put("sv4cpio", "application/x-sv4cpio");
747: mimeTypes.put("sv4crc", "application/x-sv4crc");
748: mimeTypes.put("swf", "application/x-shockwave-flash");
749: mimeTypes.put("t", "application/x-troff");
750: mimeTypes.put("tar", "application/x-tar");
751: mimeTypes.put("tcl", "application/x-tcl");
752: mimeTypes.put("tex", "application/x-tex");
753: mimeTypes.put("texi", "application/x-texinfo");
754: mimeTypes.put("texinfo", "application/x-texinfo");
755: mimeTypes.put("tif", "image/tiff");
756: mimeTypes.put("tiff", "image/tiff");
757: mimeTypes.put("tr", "application/x-troff");
758: mimeTypes.put("tsv", "text/tab-separated-values");
759: mimeTypes.put("txt", "text/plain");
760: mimeTypes.put("ulw", "audio/basic");
761: mimeTypes.put("ustar", "application/x-ustar");
762: mimeTypes.put("xbm", "image/x-xbitmap");
763: mimeTypes.put("xht", "application/xhtml+xml");
764: mimeTypes.put("xhtml", "application/xhtml+xml");
765: mimeTypes.put("xml", "text/xml");
766: mimeTypes.put("xpm", "image/x-xpixmap");
767: mimeTypes.put("xsl", "text/xml");
768: mimeTypes.put("xwd", "image/x-xwindowdump");
769: mimeTypes.put("wav", "audio/x-wav");
770: mimeTypes.put("svg", "image/svg+xml");
771: mimeTypes.put("svgz", "image/svg+xml");
772: mimeTypes.put("vsd", "application/x-visio");
773: mimeTypes.put("wbmp", "image/vnd.wap.wbmp");
774: mimeTypes.put("wml", "text/vnd.wap.wml");
775: mimeTypes.put("wmlc", "application/vnd.wap.wmlc");
776: mimeTypes.put("wmls", "text/vnd.wap.wmlscript");
777: mimeTypes.put("wmlscriptc", "application/vnd.wap.wmlscriptc");
778: mimeTypes.put("wrl", "x-world/x-vrml");
779: mimeTypes.put("Z", "application/x-compress");
780: mimeTypes.put("z", "application/x-compress");
781: mimeTypes.put("zip", "application/zip");
782: }
783:
784: /**
785: * Checks to see whether or not a client's file is out of date relative to the
786: * original.
787: */
788: private boolean isNotModified(HttpServletRequest request,
789: long ageOfServerCopy) {
790: // The age of the server copy *must* have the milliseconds truncated.
791: // Since milliseconds isn't part of the GMT format, failure to truncate
792: // will leave the file in a state where it appears constantly out of date
793: // and yet it can never get in sync because the Last-Modified date keeps
794: // truncating off the milliseconds part on its way out.
795: //
796: ageOfServerCopy -= (ageOfServerCopy % 1000);
797:
798: long ageOfClientCopy = 0;
799: String ifModifiedSince = request.getHeader("If-Modified-Since");
800: if (ifModifiedSince != null) {
801: // Rip off any additional stuff at the end, such as "; length="
802: // (IE does add this).
803: //
804: int lastSemi = ifModifiedSince.lastIndexOf(';');
805: if (lastSemi != -1) {
806: ifModifiedSince = ifModifiedSince
807: .substring(0, lastSemi);
808: }
809: ageOfClientCopy = HttpHeaders
810: .fromInternetDateFormat(ifModifiedSince);
811: }
812:
813: if (ageOfClientCopy >= ageOfServerCopy) {
814: // The client already has a good copy.
815: //
816: return true;
817: } else {
818: // The client needs a fresh copy of the requested file.
819: //
820: return false;
821: }
822: }
823:
824: private void maybeIssueXhtmlWarning(TreeLogger logger,
825: String mimeType, String path) {
826: if (!XHTML_MIME_TYPE.equals(mimeType)) {
827: return;
828: }
829:
830: String msg = "File was returned with content-type of \""
831: + mimeType
832: + "\". GWT requires browser features that are not available to "
833: + "documents with this content-type.";
834:
835: int ix = path.lastIndexOf('.');
836: if (ix >= 0 && ix < path.length()) {
837: String base = path.substring(0, ix);
838: msg += " Consider renaming \"" + path + "\" to \"" + base
839: + ".html\".";
840: }
841:
842: logger.log(TreeLogger.WARN, msg, null);
843: }
844:
845: private void sendErrorResponse(HttpServletResponse response,
846: int statusCode, String msg) throws IOException {
847: response.setContentType("text/html");
848: response.getWriter().println(msg);
849: response.setStatus(statusCode);
850: }
851:
852: /**
853: * Sets the Cache-control and Expires headers in the response based on the
854: * supplied cache time.
855: *
856: * Expires is used in addition to Cache-control for older clients or proxies
857: * which may not properly understand Cache-control.
858: *
859: * @param response the HttpServletResponse to update
860: * @param cacheTime non-negative number of seconds to cache the response; 0
861: * means specifically do not allow caching at all.
862: * @throws IllegalArgumentException if cacheTime is negative
863: */
864: private void setResponseCacheHeaders(HttpServletResponse response,
865: long cacheTime) {
866: long expires;
867: if (cacheTime < 0) {
868: throw new IllegalArgumentException("cacheTime of "
869: + cacheTime + " is negative");
870: }
871: if (cacheTime > 0) {
872: // Expire the specified seconds in the future.
873: expires = new Date().getTime() + cacheTime
874: * HttpHeaders.MS_SEC;
875: } else {
876: // Prevent caching by using a time in the past for cache expiration.
877: // Use January 2, 1970 00:00:00, to account for timezone changes
878: // in case a browser tries to convert to a local timezone first
879: // 0=Jan 1, so add 1 day's worth of milliseconds to get Jan 2
880: expires = HttpHeaders.SEC_DAY * HttpHeaders.MS_SEC;
881: }
882: response.setHeader(HttpHeaders.CACHE_CONTROL,
883: HttpHeaders.CACHE_CONTROL_MAXAGE + cacheTime);
884: String expiresString = HttpHeaders
885: .toInternetDateFormat(expires);
886: response.setHeader(HttpHeaders.EXPIRES, expiresString);
887: }
888:
889: private void streamOut(InputStream in, OutputStream out,
890: int bufferSize) throws IOException {
891: assert (bufferSize >= 0);
892:
893: byte[] buffer = new byte[bufferSize];
894: int bytesRead = 0;
895: while (true) {
896: bytesRead = in.read(buffer);
897: if (bytesRead >= 0) {
898: // Copy the bytes out.
899: out.write(buffer, 0, bytesRead);
900: } else {
901: // End of input stream.
902: return;
903: }
904: }
905: }
906:
907: private HttpServlet tryGetOrLoadServlet(TreeLogger logger,
908: ModuleDef moduleDef, String className) {
909: synchronized (loadedServletsByModuleAndClassName) {
910: String moduleAndClassName = moduleDef.getName() + "/"
911: + className;
912: HttpServlet servlet = loadedServletsByModuleAndClassName
913: .get(moduleAndClassName);
914: if (servlet != null) {
915: // Found it.
916: //
917: return servlet;
918: }
919:
920: // Try to load and instantiate it.
921: //
922: Throwable caught = null;
923: try {
924: Class<?> servletClass = Class.forName(className);
925: Object newInstance = servletClass.newInstance();
926: if (!(newInstance instanceof HttpServlet)) {
927: logger
928: .log(
929: TreeLogger.ERROR,
930: "Not compatible with HttpServlet: "
931: + className
932: + " (does your service extend RemoteServiceServlet?)",
933: null);
934: return null;
935: }
936:
937: // Success. Hang onto the instance so we can reuse it.
938: //
939: servlet = (HttpServlet) newInstance;
940:
941: // We create proxies for ServletContext and ServletConfig to enable
942: // RemoteServiceServlets to load public and generated resources via
943: // ServeletContext.getResourceAsStream()
944: //
945: ServletContext context = new HostedModeServletContextProxy(
946: getServletContext(), moduleDef, getOutputDir());
947: ServletConfig config = new HostedModeServletConfigProxy(
948: getServletConfig(), context);
949:
950: servlet.init(config);
951:
952: loadedServletsByModuleAndClassName.put(
953: moduleAndClassName, servlet);
954: return servlet;
955: } catch (ClassNotFoundException e) {
956: caught = e;
957: } catch (InstantiationException e) {
958: caught = e;
959: } catch (IllegalAccessException e) {
960: caught = e;
961: } catch (ServletException e) {
962: caught = e;
963: }
964: String msg = "Unable to instantiate '" + className + "'";
965: logger.log(TreeLogger.ERROR, msg, caught);
966: return null;
967: }
968: }
969: }
|