text: /**
   * Copyright 2009 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.
   */
  
  /**
   * @fileOverview serving static files, including js and css, and cacheing
   *               and minifying.
   *
   * Terminology Note:
   *    "path" is confusing because paths can be part of URLs and part
   *     of filesystem paths, and static files have both types of paths
   *     associated with them.  Therefore, in this module:
   *
   *      LOCALDIR or LOCALFILE refers to directories or files on the filesystem.
   *
   *      HREF is used to describe things that go in a URL.
   */
  
  import("fileutils.{readFile,readFileBytes}");
  import("yuicompressor");
  import("stringutils");
  import("varz");
  import("ejs.EJS");
  
  jimport("java.lang.System.out.println");
  
  //----------------------------------------------------------------
  // Content Type Guessing
  //----------------------------------------------------------------
  
  var _contentTypes = {
    'gif': 'image/gif',
    'png': 'image/png',
    'jpg': 'image/jpeg',
    'jpeg': 'image/jpeg',
    'css': 'text/css',
    'js': 'application/x-javascript',
    'txt': 'text/plain',
    'html': 'text/html; charset=utf-8',
    'ico': 'image/x-icon',
    'swf': 'application/x-shockwave-flash',
    'zip': 'application/zip',
    'xml': 'application/xml'
  };
  
  var _gzipableTypes = {
    'text/css': true,
    'application/x-javascript': true,
    'text/html; charset=utf-8': true
  };
  
  function _guessContentType(path) {
    var ext = path.split('.').pop().toLowerCase();
    return _contentTypes[ext] || 'text/plain';
  }
  
  //----------------------------------------------------------------
  
  function _getCache(name) {
    var m = 'faststatic';
    if (!appjet.cache[m]) {
      appjet.cache[m] = {};
    }
    var c = appjet.cache[m];
    if (!c[name]) {
      c[name] = {};
    }
    return c[name];
  }
  
  var _mtimeCheckInterval = 5000; // 5 seconds
  
  function _getMTime(f) {
    var mcache = _getCache('mtimes');
    var now = +(new Date);
    if (appjet.config.devMode ||
        !(mcache[f] && (now - mcache[f].lastCheck < _mtimeCheckInterval))) {
      var jfile = new net.appjet.oui.JarVirtualFile(f);
      if (jfile.exists() && !jfile.isDirectory()) {
        mcache[f] = {
  	lastCheck:  now,
  	mtime: jfile.lastModified()
        };
      } else {
        mcache[f] = null;
      }
    }
    if (mcache[f]) {
      return +mcache[f].mtime;
    } else {
      return null;
    }
  }
  
  function _wrapFile(localFile) {
    return {
      getPath: function() { return localFile; },
      getMTime: function() { return _getMTime(localFile); },
      getContents: function() { return _readFileAndProcess(localFile, 'string'); }
    };
  }
  
  function _readFileAndProcess(fileName, type) {
    if (fileName.slice(-8) == "_ejs.css") {
      // run CSS through EJS
      var template = readFile(fileName);
      var ejs = new EJS({text:template, name:fileName});
      var resultString = ejs.render({});
      if (type == 'bytes') {
        return new java.lang.String(resultString).getBytes("UTF-8");
      }
      else {
        return resultString;
      }
    }
    else if (type == 'string') {
      return readFile(fileName);
    }
    else if (type == 'bytes') {
      return readFileBytes(fileName);
    }
  }
  
  function _cachedFileBytes(f) {
    var mtime = _getMTime(f);
    if (!mtime) { return null; }
    var fcache = _getCache('file-bytes-cache');
    if (!(fcache[f] && (fcache[f].mtime == mtime))) {
      varz.incrementInt("faststatic-file-bytes-cache-miss");
      var bytes = _readFileAndProcess(f, 'bytes');
      if (bytes) {
        fcache[f] = {mtime: mtime, bytes: bytes};
      };
    }
    if (fcache[f] && fcache[f].bytes) {
      return fcache[f].bytes;
    } else {
      return null;
    }
  }
  
  function _shouldGzip(contentType) {
    var userAgent = request.headers["User-Agent"];
    if (! userAgent) return false;
    if (! (/Firefox/.test(userAgent) || /webkit/i.test(userAgent))) return false;
    if (! _gzipableTypes[contentType]) return false;
  
  	return request.acceptsGzip;
  }
  
  function _getCachedGzip(original, key) {
    var c = _getCache("gzipped");
    if (! c[key] || ! java.util.Arrays.equals(c[key].original, original)) {
      c[key] = {original: original,
                gzip: stringutils.gzip(original)};
    }
    return c[key].gzip;
  }
  
  function _setGzipHeader() {
    response.setHeader("Content-Encoding", "gzip");
  }
  
  //----------------------------------------------------------------
  
  /**
   * Function for serving a single static file.
   */
  function singleFileServer(localPath, opts) {
    var contentType = _guessContentType(localPath);
  
    return function() {
      (opts.cache ? response.alwaysCache() : response.neverCache());
      response.setContentType(contentType);
      var bytes = _cachedFileBytes(localPath);
      if (bytes) {
        if (_shouldGzip(contentType)) {
          bytes = _getCachedGzip(bytes, "file:"+localPath);
          _setGzipHeader();
        }
        response.writeBytes(bytes);
        return true;
      } else {
        return false;
      }
    };
  }
  
  /**
   * valid opts:
   *   alwaysCache: default false
   */
  function directoryServer(localDir, opts) {
    if (stringutils.endsWith(localDir, "/")) {
      localDir = localDir.substr(0, localDir.length-1);
    }
    return function(relpath) {
      if (stringutils.startsWith(relpath, "/")) {
        relpath = relpath.substr(1);
      }
      if (relpath.indexOf('..') != -1) {
        response.forbid();
      }
      (opts.cache ? response.alwaysCache() : response.neverCache());
      var contentType = _guessContentType(relpath);
      response.setContentType(contentType);
      var fullPath = localDir + "/" + relpath;
      var bytes = _cachedFileBytes(fullPath);
  
      if (bytes) {
        if (_shouldGzip(contentType)) {
          bytes = _getCachedGzip(bytes, "file:"+fullPath);
          _setGzipHeader();
        }
        response.writeBytes(bytes);
        return true;
      } else {
        return false;
      }
    };
  }
  
  /**
   * Serves cat files, which are concatenated versions of many files.
   */
  function compressedFileServer(opts) {
    var cfcache = _getCache('compressed-files');
    return function() {
      var key = request.path.split('/').slice(-1)[0];
      var contentType = _guessContentType(request.path);
      response.setContentType(contentType);
      response.alwaysCache();
      var data = cfcache[key];
      if (data) {
        if (_shouldGzip(contentType)) {
          data = _getCachedGzip((new java.lang.String(data)).getBytes(response.getCharacterEncoding()), "comp:"+key);
          _setGzipHeader();
          response.writeBytes(data);
        } else {
          response.write(data);
        }
        return true;
      } else {
        return false;
      }
    };
  }
  
  function getCompressedFilesKey(type, baseLocalDir, localFileList) {
    if (stringutils.endsWith(baseLocalDir, '/')) {
      baseLocalDir = baseLocalDir.substr(0, baseLocalDir.length-1);
    }
  
    var fileList = [];
    // convert passed-in file list into list of our file objects
    localFileList.forEach(function(f) {
      if (typeof(f) == 'string') {
        fileList.push(_wrapFile(baseLocalDir+'/'+f));
      } else {
        fileList.push(f);
      }
    });
  
    // have we seen this exact fileset before?
    var fsId = fileList.map(function(f) { return f.getPath(); }).join('|');
    var fsMTime = Math.max.apply(this,
  			       fileList.map(function(f) { return f.getMTime(); }));
  
    var kdcache = _getCache('fileset-keydata-cache');
    if (!(kdcache[fsId] && (kdcache[fsId].mtime == fsMTime))) {
      //println("cache miss for fileset: "+fsId);
      //println("compressing fileset...");
      kdcache[fsId] = {
        mtime: fsMTime,
        keyString: _compressFilesAndMakeKey(type, fileList)
      };
    }
    return kdcache[fsId].keyString;
  }
  
  function _compressFilesAndMakeKey(type, fileList) {
    function _compress(s) {
      if (type == 'css') {
        varz.incrementInt("faststatic-yuicompressor-compressCSS");
        return yuicompressor.compressCSS(s);
      } else if (type == 'js') {
        varz.incrementInt("faststatic-yuicompressor-compressJS");
        return yuicompressor.compressJS(s);
      } else {
        throw Error('Dont know how to compress this filetype: '+type);
      }
    }
  
    var fullstr = "";
    fileList.forEach(function(f) {
      fullstr += _compress(f.getContents());
    });
  
    fullstr = _compress(fullstr);
  
    var key = stringutils.md5(fullstr) + '.' + type;
    var cfcache = _getCache('compressed-files');
    cfcache[key] = fullstr;
    return key;
  }
  
  