From ea3b1153e52ac1513da4d634eedefb05c261039c Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Tue, 22 Dec 2009 18:03:44 +0000 Subject: move query server to a design-doc based protocol, closes COUCHDB-589 git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@893249 13f79535-47bb-0310-9956-ffa450edef68 --- share/server/render.js | 558 +++++++++++++++++++++++++------------------------ 1 file changed, 286 insertions(+), 272 deletions(-) (limited to 'share/server/render.js') diff --git a/share/server/render.js b/share/server/render.js index f147af89..e19f31c4 100644 --- a/share/server/render.js +++ b/share/server/render.js @@ -11,152 +11,115 @@ // the License. -// registerType(name, mime-type, mime-type, ...) -// -// Available in query server sandbox. TODO: The list is cleared on reset. -// This registers a particular name with the set of mimetypes it can handle. -// Whoever registers last wins. -// -// Example: -// registerType("html", "text/html; charset=utf-8"); - -mimesByKey = {}; -keysByMime = {}; -registerType = function() { - var mimes = [], key = arguments[0]; - for (var i=1; i < arguments.length; i++) { - mimes.push(arguments[i]); - }; - mimesByKey[key] = mimes; - for (var i=0; i < mimes.length; i++) { - keysByMime[mimes[i]] = key; - }; -}; - -// Some default types -// Ported from Ruby on Rails -// Build list of Mime types for HTTP responses -// http://www.iana.org/assignments/media-types/ -// http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb - -registerType("all", "*/*"); -registerType("text", "text/plain; charset=utf-8", "txt"); -registerType("html", "text/html; charset=utf-8"); -registerType("xhtml", "application/xhtml+xml", "xhtml"); -registerType("xml", "application/xml", "text/xml", "application/x-xml"); -registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); -registerType("css", "text/css"); -registerType("ics", "text/calendar"); -registerType("csv", "text/csv"); -registerType("rss", "application/rss+xml"); -registerType("atom", "application/atom+xml"); -registerType("yaml", "application/x-yaml", "text/yaml"); -// just like Rails -registerType("multipart_form", "multipart/form-data"); -registerType("url_encoded_form", "application/x-www-form-urlencoded"); -// http://www.ietf.org/rfc/rfc4627.txt -registerType("json", "application/json", "text/x-json"); - -// Start chunks -var startResp = {}; -function start(resp) { - startResp = resp || {}; -}; - -function sendStart() { - startResp = applyContentType((startResp || {}), responseContentType); - respond(["start", chunks, startResp]); - chunks = []; - startResp = {}; -} - -function applyContentType(resp, responseContentType) { - resp["headers"] = resp["headers"] || {}; - if (responseContentType) { - resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; - } - return resp; -} - -// Send chunk -var chunks = []; -function send(chunk) { - chunks.push(chunk.toString()); -}; - -function blowChunks(label) { - respond([label||"chunks", chunks]); - chunks = []; -}; - -var gotRow = false, lastRow = false; -function getRow() { - if (lastRow) return null; - if (!gotRow) { - gotRow = true; - sendStart(); - } else { - blowChunks(); - } - var line = readline(); - var json = eval(line); - if (json[0] == "list_end") { - lastRow = true; - return null; - } - if (json[0] != "list_row") { - respond({ - error: "query_server_error", - reason: "not a row '" + json[0] + "'"}); - quit(); - } - return json[1]; -}; - -var mimeFuns = [], providesUsed, responseContentType; -function provides(type, fun) { - providesUsed = true; - mimeFuns.push([type, fun]); -}; - -function runProvides(req) { - var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; - if (req.query && req.query.format) { - bestKey = req.query.format; - responseContentType = mimesByKey[bestKey][0]; - } else if (accept) { - // log("using accept header: "+accept); - mimeFuns.reverse().forEach(function(mimeFun) { - var mimeKey = mimeFun[0]; - if (mimesByKey[mimeKey]) { - supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); - } - }); - responseContentType = Mimeparse.bestMatch(supportedMimes, accept); - bestKey = keysByMime[responseContentType]; - } else { - // just do the first one - bestKey = mimeFuns[0][0]; - responseContentType = mimesByKey[bestKey][0]; +var Mime = (function() { + // registerType(name, mime-type, mime-type, ...) + // + // Available in query server sandbox. TODO: The list is cleared on reset. + // This registers a particular name with the set of mimetypes it can handle. + // Whoever registers last wins. + // + // Example: + // registerType("html", "text/html; charset=utf-8"); + + var mimesByKey = {}; + var keysByMime = {}; + function registerType() { + var mimes = [], key = arguments[0]; + for (var i=1; i < arguments.length; i++) { + mimes.push(arguments[i]); + }; + mimesByKey[key] = mimes; + for (var i=0; i < mimes.length; i++) { + keysByMime[mimes[i]] = key; + }; } + + // Some default types + // Ported from Ruby on Rails + // Build list of Mime types for HTTP responses + // http://www.iana.org/assignments/media-types/ + // http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb + + registerType("all", "*/*"); + registerType("text", "text/plain; charset=utf-8", "txt"); + registerType("html", "text/html; charset=utf-8"); + registerType("xhtml", "application/xhtml+xml", "xhtml"); + registerType("xml", "application/xml", "text/xml", "application/x-xml"); + registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); + registerType("css", "text/css"); + registerType("ics", "text/calendar"); + registerType("csv", "text/csv"); + registerType("rss", "application/rss+xml"); + registerType("atom", "application/atom+xml"); + registerType("yaml", "application/x-yaml", "text/yaml"); + // just like Rails + registerType("multipart_form", "multipart/form-data"); + registerType("url_encoded_form", "application/x-www-form-urlencoded"); + // http://www.ietf.org/rfc/rfc4627.txt + registerType("json", "application/json", "text/x-json"); - if (bestKey) { - for (var i=0; i < mimeFuns.length; i++) { - if (mimeFuns[i][0] == bestKey) { - bestFun = mimeFuns[i][1]; - break; - } + + var mimeFuns = []; + function provides(type, fun) { + Mime.providesUsed = true; + mimeFuns.push([type, fun]); + }; + + function resetProvides() { + // set globals + Mime.providesUsed = false; + mimeFuns = []; + Mime.responseContentType = null; + }; + + function runProvides(req) { + var supportedMimes = [], bestFun, bestKey = null, accept = req.headers["Accept"]; + if (req.query && req.query.format) { + bestKey = req.query.format; + Mime.responseContentType = mimesByKey[bestKey][0]; + } else if (accept) { + // log("using accept header: "+accept); + mimeFuns.reverse().forEach(function(mimeFun) { + var mimeKey = mimeFun[0]; + if (mimesByKey[mimeKey]) { + supportedMimes = supportedMimes.concat(mimesByKey[mimeKey]); + } + }); + Mime.responseContentType = Mimeparse.bestMatch(supportedMimes, accept); + bestKey = keysByMime[Mime.responseContentType]; + } else { + // just do the first one + bestKey = mimeFuns[0][0]; + Mime.responseContentType = mimesByKey[bestKey][0]; + } + + if (bestKey) { + for (var i=0; i < mimeFuns.length; i++) { + if (mimeFuns[i][0] == bestKey) { + bestFun = mimeFuns[i][1]; + break; + } + }; }; + + if (bestFun) { + return bestFun(); + } else { + var supportedTypes = mimeFuns.map(function(mf) {return mimesByKey[mf[0]].join(', ') || mf[0]}); + throw(["error","not_acceptable", + "Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')]); + } }; + - if (bestFun) { - // log("responding with: "+bestKey); - return bestFun(); - } else { - var supportedTypes = mimeFuns.map(function(mf) {return mimesByKey[mf[0]].join(', ') || mf[0]}); - throw({error:"not_acceptable", reason:"Content-Type "+(accept||bestKey)+" not supported, try one of: "+supportedTypes.join(', ')}); - } -}; + return { + registerType : registerType, + provides : provides, + resetProvides : resetProvides, + runProvides : runProvides + } +})(); + @@ -167,151 +130,202 @@ function runProvides(req) { //// //// -var Render = { - show : function(funSrc, doc, req) { - var showFun = compileFunction(funSrc); - runShow(showFun, doc, req, funSrc); - }, - update : function(funSrc, doc, req) { - var upFun = compileFunction(funSrc); - runUpdate(upFun, doc, req, funSrc); - }, - list : function(head, req) { - runList(funs[0], head, req, funsrc[0]); +var Render = (function() { + var chunks = []; + + + // Start chunks + var startResp = {}; + function start(resp) { + startResp = resp || {}; + }; + + function sendStart() { + startResp = applyContentType((startResp || {}), Mime.responseContentType); + respond(["start", chunks, startResp]); + chunks = []; + startResp = {}; } -}; -function maybeWrapResponse(resp) { - var type = typeof resp; - if ((type == "string") || (type == "xml")) { - return {body:resp}; - } else { + function applyContentType(resp, responseContentType) { + resp["headers"] = resp["headers"] || {}; + if (responseContentType) { + resp["headers"]["Content-Type"] = resp["headers"]["Content-Type"] || responseContentType; + } return resp; } -}; -function resetProvides() { - // set globals - providesUsed = false; - mimeFuns = []; - responseContentType = null; -}; + function send(chunk) { + chunks.push(chunk.toString()); + }; + + function blowChunks(label) { + respond([label||"chunks", chunks]); + chunks = []; + }; + + var gotRow = false, lastRow = false; + function getRow() { + if (lastRow) return null; + if (!gotRow) { + gotRow = true; + sendStart(); + } else { + blowChunks(); + } + var line = readline(); + var json = eval('('+line+')'); + if (json[0] == "list_end") { + lastRow = true; + return null; + } + if (json[0] != "list_row") { + throw(["fatal", "list_error", "not a row '" + json[0] + "'"]); + } + return json[1]; + }; + + + function maybeWrapResponse(resp) { + var type = typeof resp; + if ((type == "string") || (type == "xml")) { + return {body:resp}; + } else { + return resp; + } + }; -// from http://javascript.crockford.com/remedial.html -function typeOf(value) { + // from http://javascript.crockford.com/remedial.html + function typeOf(value) { var s = typeof value; if (s === 'object') { - if (value) { - if (value instanceof Array) { - s = 'array'; - } - } else { - s = 'null'; + if (value) { + if (value instanceof Array) { + s = 'array'; } + } else { + s = 'null'; + } } return s; -}; - -function runShow(showFun, doc, req, funSrc) { - try { - resetProvides(); - var resp = showFun.apply(null, [doc, req]); - - if (providesUsed) { - resp = runProvides(req); - resp = applyContentType(maybeWrapResponse(resp), responseContentType); - } + }; - var type = typeOf(resp); - if (type == 'object' || type == 'string') { - respond(["resp", maybeWrapResponse(resp)]); - } else { - renderError("undefined response from show function"); - } - } catch(e) { - respondError(e, funSrc, true); - } -}; - -function runUpdate(renderFun, doc, req, funSrc) { - try { - var result = renderFun.apply(null, [doc, req]); - var doc = result[0]; - var resp = result[1]; - if (resp) { - respond(["up", doc, maybeWrapResponse(resp)]); - } else { - renderError("undefined response from update function"); + function runShow(fun, ddoc, args) { + try { + Mime.resetProvides(); + var resp = fun.apply(ddoc, args); + + if (Mime.providesUsed) { + resp = Mime.runProvides(args[1]); + resp = applyContentType(maybeWrapResponse(resp), Mime.responseContentType); + } + + var type = typeOf(resp); + if (type == 'object' || type == 'string') { + respond(["resp", maybeWrapResponse(resp)]); + } else { + throw(["error", "render_error", "undefined response from show function"]); + } + } catch(e) { + renderError(e, fun.toSource()); } - } catch(e) { - respondError(e, funSrc, true); - } -}; - -function resetList() { - gotRow = false; - lastRow = false; - chunks = []; - startResp = {}; -}; - -function runList(listFun, head, req, funSrc) { - try { - if (listFun.arity > 2) { - throw("the list API has changed for CouchDB 0.10, please upgrade your code"); + }; + + function runUpdate(fun, ddoc, args) { + try { + var verb = args[1].verb; + // for analytics logging applications you might want to remove the next line + if (verb == "GET") throw(["error","method_not_allowed","Update functions do not allow GET"]); + var result = fun.apply(ddoc, args); + var doc = result[0]; + var resp = result[1]; + var type = typeOf(resp); + if (type == 'object' || type == 'string') { + respond(["up", doc, maybeWrapResponse(resp)]); + } else { + throw(["error", "render_error", "undefined response from update function"]); + } + } catch(e) { + renderError(e, fun.toSource()); } - - resetProvides(); - resetList(); - - var tail = listFun.apply(null, [head, req]); - - if (providesUsed) { - tail = runProvides(req); + }; + + function resetList() { + gotRow = false; + lastRow = false; + chunks = []; + startResp = {}; + }; + + function runList(listFun, ddoc, args) { + try { + Mime.resetProvides(); + resetList(); + head = args[0] + req = args[1] + var tail = listFun.apply(ddoc, args); + + if (Mime.providesUsed) { + tail = Mime.runProvides(req); + } + if (!gotRow) getRow(); + if (typeof tail != "undefined") { + chunks.push(tail); + } + blowChunks("end"); + } catch(e) { + renderError(e, listFun.toSource()); } - - if (!gotRow) { - getRow(); + }; + + function renderError(e, funSrc) { + if (e.error && e.reason || e[0] == "error" || e[0] == "fatal") { + throw(e); + } else { + var logMessage = "function raised error: "+e.toSource()+" \nstacktrace: "+e.stack; + log(logMessage); + throw(["error", "render_error", logMessage]); } - if (typeof tail != "undefined") { - chunks.push(tail); + }; + + function escapeHTML(string) { + return string && string.replace(/&/g, "&") + .replace(//g, ">"); + }; + + + return { + start : start, + send : send, + getRow : getRow, + show : function(fun, ddoc, args) { + // var showFun = Couch.compileFunction(funSrc); + runShow(fun, ddoc, args); + }, + update : function(fun, ddoc, args) { + // var upFun = Couch.compileFunction(funSrc); + runUpdate(fun, ddoc, args); + }, + list : function(fun, ddoc, args) { + runList(fun, ddoc, args); } - blowChunks("end"); - } catch(e) { - respondError(e, funSrc, false); - } -}; - -function renderError(m) { - respond({error : "render_error", reason : m}); -} - -function respondError(e, funSrc, htmlErrors) { - if (e.error && e.reason) { - respond(e); - } else { - var logMessage = "function raised error: "+e.toString(); - log(logMessage); - log("stacktrace: "+e.stack); - var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; - renderError(errorMessage); - } -} - -function escapeHTML(string) { - return string.replace(/&/g, "&") - .replace(//g, ">"); -} - -function htmlRenderError(e, funSrc) { - var msg = ["

Render Error

", - "

JavaScript function raised error: ", - e.toString(), - "

Stacktrace:

",
-    escapeHTML(e.stack),
-    "

Function source:

",
-    escapeHTML(funSrc),
-    "
"].join(''); - return {body:msg}; -}; + }; +})(); + +// send = Render.send; +// getRow = Render.getRow; +// start = Render.start; + +// unused. this will be handled in the Erlang side of things. +// function htmlRenderError(e, funSrc) { +// var msg = ["

Render Error

", +// "

JavaScript function raised error: ", +// e.toString(), +// "

Stacktrace:

",
+//     escapeHTML(e.stack),
+//     "

Function source:

",
+//     escapeHTML(funSrc),
+//     "
"].join(''); +// return {body:msg}; +// }; -- cgit v1.2.3