You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by jc...@apache.org on 2009/01/27 21:46:40 UTC

svn commit: r738237 - in /couchdb/trunk: share/server/main.js share/www/script/couch_tests.js src/couchdb/couch_doc.erl src/couchdb/couch_httpd.erl src/couchdb/couch_httpd_db.erl src/couchdb/couch_httpd_show.erl src/couchdb/couch_query_servers.erl

Author: jchris
Date: Tue Jan 27 20:46:39 2009
New Revision: 738237

URL: http://svn.apache.org/viewvc?rev=738237&view=rev
Log:
Improved etag handling for show funcs and db_doc requests; main.js cleanup (baby steps); null doc allowed for show funcs

Modified:
    couchdb/trunk/share/server/main.js
    couchdb/trunk/share/www/script/couch_tests.js
    couchdb/trunk/src/couchdb/couch_doc.erl
    couchdb/trunk/src/couchdb/couch_httpd.erl
    couchdb/trunk/src/couchdb/couch_httpd_db.erl
    couchdb/trunk/src/couchdb/couch_httpd_show.erl
    couchdb/trunk/src/couchdb/couch_query_servers.erl

Modified: couchdb/trunk/share/server/main.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/server/main.js?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/share/server/main.js [utf-8] (original)
+++ couchdb/trunk/share/server/main.js [utf-8] Tue Jan 27 20:46:39 2009
@@ -29,7 +29,12 @@
 }
 
 log = function(message) {
-  print(toJSON({log: toJSON(message)}));  
+  if (typeof message == "undefined") {
+    message = "Error: attempting to log message of 'undefined'.";
+  } else if (typeof message != "string") {
+    message = toJSON(message);
+  }
+  print(toJSON({log: message}));
 }
 
 // mimeparse.js
@@ -135,7 +140,7 @@
 
 // this function provides a shortcut for managing responses by Accept header
 respondWith = function(req, responders) {
-  var accept = req.headers["Accept"];
+  var bestKey = null, accept = req.headers["Accept"];
   if (accept) {
     var provides = [];
     for (key in responders) {
@@ -144,19 +149,17 @@
       }
     }
     var bestMime = Mimeparse.bestMatch(provides, accept);
-    var bestKey = keysByMime[bestMime];
-    var rFunc = responders[bestKey];
-    if (rFunc) {
-      var resp = maybeWrapResponse(rFunc());
-      resp["headers"] = resp["headers"] || {};
-      resp["headers"]["Content-Type"] = bestMime;
-      return resp;
-    }
+    bestKey = keysByMime[bestMime];
+  }
+  var rFunc = responders[bestKey || responders.fallback || "html"];
+  if (rFunc) {      
+    var resp = maybeWrapResponse(rFunc());
+    resp["headers"] = resp["headers"] || {};
+    resp["headers"]["Content-Type"] = bestMime;
+    respond(resp);
+  } else {
+    throw({code:406, body:"Not Acceptable: "+accept});    
   }
-  if (responders.fallback) {
-    return responders[responders.fallback]();
-  } 
-  throw({code:406, body:"Not Acceptable: "+accept});
 }
 
 // whoever registers last wins.
@@ -378,20 +381,30 @@
   }
 };
 
+var responseSent;
 function runRenderFunction(renderFun, args) {
+  responseSent = false;
   try {
     var resp = renderFun.apply(null, args);
-    respond(maybeWrapResponse(resp)); 
+    if (!responseSent) {
+      if (resp) {
+        respond(maybeWrapResponse(resp));       
+      } else {
+        respond({error:"render_error",reason:"undefined response from render function"});
+      }      
+    }
   } catch(e) {
     log("function raised error: "+e.toString());
     log("stacktrace: "+e.stack);
+    respond({error:"render_error",reason:e});
   }
 };
 
 // prints the object as JSON, and rescues and logs any toJSON() related errors
 function respond(obj) {
+  responseSent = true;
   try {
-    print(toJSON(obj));    
+    print(toJSON(obj));  
   } catch(e) {
     log("Error converting object to JSON: " + e.toString());
   }

Modified: couchdb/trunk/share/www/script/couch_tests.js
URL: http://svn.apache.org/viewvc/couchdb/trunk/share/www/script/couch_tests.js?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/share/www/script/couch_tests.js [utf-8] (original)
+++ couchdb/trunk/share/www/script/couch_tests.js [utf-8] Tue Jan 27 20:46:39 2009
@@ -2307,10 +2307,12 @@
        _id:"_design/template",
        language: "javascript",
        shows: {
-         "hello" : stringFun(function() { 
-           return {
-             body : "Hello World"
-           };
+         "hello" : stringFun(function(doc) { 
+           if (doc) {
+             return "Hello World";             
+           } else {
+             return "Empty World";
+           }
          }),
          "just-name" : stringFun(function(doc, req) {
            return {
@@ -2410,6 +2412,11 @@
      // hello template world
      xhr = CouchDB.request("GET", "/test_suite_db/_show/template/hello/"+docid);
      T(xhr.responseText == "Hello World");
+
+     // hello template world (no docid)
+     xhr = CouchDB.request("GET", "/test_suite_db/_show/template/hello");
+     T(xhr.responseText == "Empty World");
+
      
      // show with doc
      xhr = CouchDB.request("GET", "/test_suite_db/_show/template/just-name/"+docid);

Modified: couchdb/trunk/src/couchdb/couch_doc.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_doc.erl?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_doc.erl (original)
+++ couchdb/trunk/src/couchdb/couch_doc.erl Tue Jan 27 20:46:39 2009
@@ -147,7 +147,7 @@
         attachments = Bins
         };
 
-from_json_obj(Other) ->
+from_json_obj(_Other) ->
     throw({invalid_json_object, "Document must be a JSON object"}).
 
 to_doc_info(FullDocInfo) ->

Modified: couchdb/trunk/src/couchdb/couch_httpd.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd.erl?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd.erl Tue Jan 27 20:46:39 2009
@@ -17,7 +17,7 @@
 
 -export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1]).
 -export([verify_is_server_admin/1,unquote/1,quote/1,recv/2]).
--export([parse_form/1,json_body/1,body/1,doc_etag/1]).
+-export([parse_form/1,json_body/1,body/1,doc_etag/1, make_etag/1, etag_respond/3]).
 -export([primary_header_value/2,partition/1,serve_file/3]).
 -export([start_chunked_response/3,send_chunk/2]).
 -export([start_json_response/2, start_json_response/3, end_json_response/1]).
@@ -261,7 +261,29 @@
     ?JSON_DECODE(MochiReq:recv_body(?MAX_DOC_SIZE)).
 
 doc_etag(#doc{revs=[DiskRev|_]}) ->
-     "\"" ++ binary_to_list(DiskRev) ++ "\"".
+    "\"" ++ binary_to_list(DiskRev) ++ "\"".
+
+make_etag(Term) ->
+    <<SigInt:128/integer>> = erlang:md5(term_to_binary(Term)),
+    list_to_binary("\"" ++ lists:flatten(io_lib:format("~.36B",[SigInt])) ++ "\"").
+
+etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
+    etag_match(Req, binary_to_list(CurrentEtag));
+
+etag_match(Req, CurrentEtag) ->
+    EtagsToMatch = string:tokens(
+        couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
+    lists:member(CurrentEtag, EtagsToMatch).
+
+etag_respond(Req, CurrentEtag, RespFun) ->
+    case etag_match(Req, CurrentEtag) of
+    true ->
+        % the client has this in their cache.
+        couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
+    false ->
+        % Run the function.
+        RespFun()
+    end.
 
 verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
     case lists:member(<<"_admin">>, Roles) of

Modified: couchdb/trunk/src/couchdb/couch_httpd_db.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_db.erl?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_db.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_db.erl Tue Jan 27 20:46:39 2009
@@ -387,20 +387,13 @@
     [] ->
         Doc = couch_doc_open(Db, DocId, Rev, Options),
         DiskEtag = couch_httpd:doc_etag(Doc),
-        EtagsToMatch = string:tokens(
-                    couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
-        case lists:member(DiskEtag, EtagsToMatch) of
-        true ->
-            % the client has this in their cache.
-            couch_httpd:send_response(Req, 304, [{"Etag", DiskEtag}], <<>>);
-        false ->
-            Headers =
-            case Doc#doc.meta of
+        couch_httpd:etag_respond(Req, DiskEtag, fun() -> 
+            Headers = case Doc#doc.meta of
             [] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta
             _ -> []
             end,
-            send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
-        end;
+            send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))            
+        end);
     _ ->
         {ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options),
         {ok, Resp} = start_json_response(Req, 200),

Modified: couchdb/trunk/src/couchdb/couch_httpd_show.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_httpd_show.erl?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_httpd_show.erl (original)
+++ couchdb/trunk/src/couchdb/couch_httpd_show.erl Tue Jan 27 20:46:39 2009
@@ -33,6 +33,16 @@
     Doc = couch_httpd_db:couch_doc_open(Db, Docid, [], []),
     send_doc_show_response(Lang, ShowSrc, Doc, Req, Db);
 
+handle_doc_show_req(#httpd{
+        method='GET',
+        path_parts=[_, _, DesignName, ShowName]
+    }=Req, Db) ->
+    DesignId = <<"_design/", DesignName/binary>>,
+    #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, [], []),
+    Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>),
+    ShowSrc = get_nested_json_value({Props}, [<<"shows">>, ShowName]),
+    send_doc_show_response(Lang, ShowSrc, nil, Req, Db);
+
 handle_doc_show_req(#httpd{method='GET'}=Req, _Db) ->
     send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>);
 
@@ -158,45 +168,32 @@
         throw(Error)
     end.
 
+send_doc_show_response(Lang, ShowSrc, nil, #httpd{mochi_req=MReq}=Req, Db) ->
+    % compute etag with no doc
+    Headers = MReq:get(headers),
+    Hlist = mochiweb_headers:to_list(Headers),
+    Accept = proplists:get_value('Accept', Hlist),
+    CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept}),
+    couch_httpd:etag_respond(Req, CurrentEtag, fun() -> 
+        ExternalResp = couch_query_servers:render_doc_show(Lang, ShowSrc, 
+            nil, Req, Db),
+        JsonResp = apply_etag(ExternalResp, CurrentEtag),
+        couch_httpd_external:send_external_response(Req, JsonResp)
+    end);
 
 send_doc_show_response(Lang, ShowSrc, #doc{revs=[DocRev|_]}=Doc, #httpd{mochi_req=MReq}=Req, Db) ->
-    % make a term with etag-effecting Req components, but not always changing ones.
+    % calculate the etag
     Headers = MReq:get(headers),
     Hlist = mochiweb_headers:to_list(Headers),
     Accept = proplists:get_value('Accept', Hlist),
-    <<SigInt:128/integer>> = erlang:md5(term_to_binary({Lang, ShowSrc, DocRev, Accept})),
-    CurrentEtag = list_to_binary("\"" ++ lists:flatten(io_lib:format("form_~.36B",[SigInt])) ++ "\""),
-    EtagsToMatch = string:tokens(
-                couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
-    % We know our etag now                
-    case lists:member(binary_to_list(CurrentEtag), EtagsToMatch) of
-    true ->
-        % the client has this in their cache.
-        couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
-    false ->
-        % Run the external form renderer.
-        {JsonResponse} = couch_query_servers:render_doc_show(Lang, ShowSrc, Doc, Req, Db),
-        % Here we embark on the delicate task of replacing or creating the  
-        % headers on the JsonResponse object. We need to control the Etag and 
-        % Vary headers. If the external function controls the Etag, we'd have to 
-        % run it to check for a match, which sort of defeats the purpose.
-        JsonResponse2 = case proplists:get_value(<<"headers">>, JsonResponse, nil) of
-        nil ->
-            % no JSON headers
-            % add our Etag and Vary headers to the response
-            [{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | JsonResponse];
-        {JsonHeaders} ->
-            [case Field of
-            {<<"headers">>, {JsonHeaders}} -> % add our headers
-                JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders),
-                JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
-                {<<"headers">>, {JsonHeadersVaried}};
-            _ -> % skip non-header fields
-                Field
-            end || Field <- JsonResponse]
-        end,
-        couch_httpd_external:send_external_response(Req, {JsonResponse2})    
-    end.
+    CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, DocRev, Accept}),
+    % We know our etag now    
+    couch_httpd:etag_respond(Req, CurrentEtag, fun() -> 
+        ExternalResp = couch_query_servers:render_doc_show(Lang, ShowSrc, 
+            Doc, Req, Db),
+        JsonResp = apply_etag(ExternalResp, CurrentEtag),
+        couch_httpd_external:send_external_response(Req, JsonResp)
+    end).
 
 set_or_replace_header(H, L) ->
     set_or_replace_header(H, L, []).
@@ -209,4 +206,25 @@
     set_or_replace_header({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
 set_or_replace_header({Key, NewValue}, [], Acc) ->
     % end of list, add ours
-    [{Key, NewValue}|Acc].
\ No newline at end of file
+    [{Key, NewValue}|Acc].
+
+apply_etag({ExternalResponse}, CurrentEtag) ->
+    % Here we embark on the delicate task of replacing or creating the  
+    % headers on the JsonResponse object. We need to control the Etag and 
+    % Vary headers. If the external function controls the Etag, we'd have to 
+    % run it to check for a match, which sort of defeats the purpose.
+    case proplists:get_value(<<"headers">>, ExternalResponse, nil) of
+    nil ->
+        % no JSON headers
+        % add our Etag and Vary headers to the response
+        {[{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | ExternalResponse]};
+    {JsonHeaders} ->
+        {[case Field of
+        {<<"headers">>, {JsonHeaders}} -> % add our headers
+            JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders),
+            JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
+            {<<"headers">>, {JsonHeadersVaried}};
+        _ -> % skip non-header fields
+            Field
+        end || Field <- ExternalResponse]}
+    end.

Modified: couchdb/trunk/src/couchdb/couch_query_servers.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/couchdb/couch_query_servers.erl?rev=738237&r1=738236&r2=738237&view=diff
==============================================================================
--- couchdb/trunk/src/couchdb/couch_query_servers.erl (original)
+++ couchdb/trunk/src/couchdb/couch_query_servers.erl Tue Jan 27 20:46:39 2009
@@ -125,7 +125,10 @@
 
 render_doc_show(Lang, ShowSrc, Doc, Req, Db) ->
     Pid = get_os_process(Lang),
-    JsonDoc = couch_doc:to_json_obj(Doc, [revs]),
+    JsonDoc = case Doc of
+        nil -> null;
+        _ -> couch_doc:to_json_obj(Doc, [revs])
+    end,
     JsonReq = couch_httpd_external:json_req_obj(Req, Db),
     try couch_os_process:prompt(Pid, 
         [<<"show_doc">>, ShowSrc, JsonDoc, JsonReq]) of