/**
* 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;
}