| % 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. |
| |
| -module(couch_mrview_show). |
| |
| -export([ |
| handle_doc_show_req/3, |
| handle_doc_update_req/3, |
| handle_view_list_req/3, |
| list_cb/2 |
| ]). |
| |
| -include_lib("couch/include/couch_db.hrl"). |
| -include_lib("couch_mrview/include/couch_mrview.hrl"). |
| |
| % /db/_design/foo/_show/bar/docid |
| % show converts a json doc to a response of any content-type. |
| % it looks up the doc an then passes it to the query server. |
| % then it sends the response from the query server to the http client. |
| |
| maybe_open_doc(Db, DocId) -> |
| case catch couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of |
| #doc{} = Doc -> Doc; |
| {not_found, _} -> nil |
| end. |
| |
| handle_doc_show_req(#httpd{ |
| path_parts=[_, _, _, _, ShowName, DocId] |
| }=Req, Db, DDoc) -> |
| |
| % open the doc |
| Doc = maybe_open_doc(Db, DocId), |
| |
| % we don't handle revs here b/c they are an internal api |
| % returns 404 if there is no doc with DocId |
| handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId); |
| |
| handle_doc_show_req(#httpd{ |
| path_parts=[_, _, _, _, ShowName, DocId|Rest] |
| }=Req, Db, DDoc) -> |
| |
| DocParts = [DocId|Rest], |
| DocId1 = ?l2b(string:join([?b2l(P)|| P <- DocParts], "/")), |
| |
| % open the doc |
| Doc = maybe_open_doc(Db, DocId1), |
| |
| % we don't handle revs here b/c they are an internal api |
| % pass 404 docs to the show function |
| handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId1); |
| |
| handle_doc_show_req(#httpd{ |
| path_parts=[_, _, _, _, ShowName] |
| }=Req, Db, DDoc) -> |
| % with no docid the doc is nil |
| handle_doc_show(Req, Db, DDoc, ShowName, nil); |
| |
| handle_doc_show_req(Req, _Db, _DDoc) -> |
| chttpd:send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>). |
| |
| handle_doc_show(Req, Db, DDoc, ShowName, Doc) -> |
| handle_doc_show(Req, Db, DDoc, ShowName, Doc, null). |
| |
| handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId) -> |
| % get responder for ddoc/showname |
| CurrentEtag = show_etag(Req, Doc, DDoc, []), |
| chttpd:etag_respond(Req, CurrentEtag, fun() -> |
| JsonReq = chttpd_external:json_req_obj(Req, Db, DocId), |
| JsonDoc = couch_query_servers:json_doc(Doc), |
| [<<"resp">>, ExternalResp] = |
| couch_query_servers:ddoc_prompt(DDoc, [<<"shows">>, ShowName], |
| [JsonDoc, JsonReq]), |
| JsonResp = apply_etag(ExternalResp, CurrentEtag), |
| chttpd_external:send_external_response(Req, JsonResp) |
| end). |
| |
| |
| show_etag(#httpd{user_ctx=UserCtx}=Req, Doc, DDoc, More) -> |
| Accept = chttpd:header_value(Req, "Accept"), |
| DocPart = case Doc of |
| nil -> nil; |
| Doc -> chttpd:doc_etag(Doc) |
| end, |
| chttpd:make_etag({chttpd:doc_etag(DDoc), DocPart, Accept, |
| {UserCtx#user_ctx.name, UserCtx#user_ctx.roles}, More}). |
| |
| % updates a doc based on a request |
| % handle_doc_update_req(#httpd{method = 'GET'}=Req, _Db, _DDoc) -> |
| % % anything but GET |
| % send_method_not_allowed(Req, "POST,PUT,DELETE,ETC"); |
| |
| % This call is creating a new doc using an _update function to |
| % modify the provided request body. |
| % /db/_design/foo/_update/bar |
| handle_doc_update_req(#httpd{ |
| path_parts=[_, _, _, _, UpdateName] |
| }=Req, Db, DDoc) -> |
| send_doc_update_response(Req, Db, DDoc, UpdateName, nil, null); |
| |
| % /db/_design/foo/_update/bar/docid |
| handle_doc_update_req(#httpd{ |
| path_parts=[_, _, _, _, UpdateName | DocIdParts] |
| }=Req, Db, DDoc) -> |
| DocId = ?l2b(string:join([?b2l(P) || P <- DocIdParts], "/")), |
| Doc = maybe_open_doc(Db, DocId), |
| send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId); |
| |
| |
| handle_doc_update_req(Req, _Db, _DDoc) -> |
| chttpd:send_error(Req, 404, <<"update_error">>, <<"Invalid path.">>). |
| |
| send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) -> |
| JsonReq = chttpd_external:json_req_obj(Req, Db, DocId), |
| JsonDoc = couch_query_servers:json_doc(Doc), |
| Cmd = [<<"updates">>, UpdateName], |
| UpdateResp = couch_query_servers:ddoc_prompt(DDoc, Cmd, [JsonDoc, JsonReq]), |
| JsonResp = case UpdateResp of |
| [<<"up">>, {NewJsonDoc}, {JsonResp0}] -> |
| case chttpd:header_value( |
| Req, "X-Couch-Full-Commit", "false") of |
| "true" -> |
| Options = [full_commit, {user_ctx, Req#httpd.user_ctx}]; |
| _ -> |
| Options = [{user_ctx, Req#httpd.user_ctx}] |
| end, |
| NewDoc = couch_doc:from_json_obj({NewJsonDoc}), |
| couch_doc:validate_docid(NewDoc#doc.id), |
| {ok, NewRev} = couch_db:update_doc(Db, NewDoc, Options), |
| NewRevStr = couch_doc:rev_to_str(NewRev), |
| {JsonResp1} = apply_headers(JsonResp0, [ |
| {<<"X-Couch-Update-NewRev">>, NewRevStr}, |
| {<<"X-Couch-Id">>, NewDoc#doc.id} |
| ]), |
| {[{<<"code">>, 201} | JsonResp1]}; |
| [<<"up">>, _Other, {JsonResp0}] -> |
| {[{<<"code">>, 200} | JsonResp0]} |
| end, |
| % todo set location field |
| chttpd_external:send_external_response(Req, JsonResp). |
| |
| |
| handle_view_list_req(#httpd{method=Method}=Req, Db, DDoc) |
| when Method =:= 'GET' orelse Method =:= 'OPTIONS' -> |
| case Req#httpd.path_parts of |
| [_, _, _DName, _, LName, VName] -> |
| % Same design doc for view and list |
| handle_view_list(Req, Db, DDoc, LName, DDoc, VName, undefined); |
| [_, _, _, _, LName, DName, VName] -> |
| % Different design docs for view and list |
| VDocId = <<"_design/", DName/binary>>, |
| {ok, VDDoc} = couch_db:open_doc(Db, VDocId, [ejson_body]), |
| handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, undefined); |
| _ -> |
| chttpd:send_error(Req, 404, <<"list_error">>, <<"Bad path.">>) |
| end; |
| handle_view_list_req(#httpd{method='POST'}=Req, Db, DDoc) -> |
| chttpd:validate_ctype(Req, "application/json"), |
| {Props} = chttpd:json_body_obj(Req), |
| Keys = proplists:get_value(<<"keys">>, Props), |
| case Req#httpd.path_parts of |
| [_, _, _DName, _, LName, VName] -> |
| handle_view_list(Req, Db, DDoc, LName, DDoc, VName, Keys); |
| [_, _, _, _, LName, DName, VName] -> |
| % Different design docs for view and list |
| VDocId = <<"_design/", DName/binary>>, |
| {ok, VDDoc} = couch_db:open_doc(Db, VDocId, [ejson_body]), |
| handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, Keys); |
| _ -> |
| chttpd:send_error(Req, 404, <<"list_error">>, <<"Bad path.">>) |
| end; |
| handle_view_list_req(Req, _Db, _DDoc) -> |
| chttpd:send_method_not_allowed(Req, "GET,POST,HEAD"). |
| |
| |
| handle_view_list(Req, Db, DDoc, LName, VDDoc, VName, Keys) -> |
| Args0 = couch_mrview_http:parse_params(Req, Keys), |
| ETagFun = fun(BaseSig, Acc0) -> |
| UserCtx = Req#httpd.user_ctx, |
| Name = UserCtx#user_ctx.name, |
| Roles = UserCtx#user_ctx.roles, |
| Accept = chttpd:header_value(Req, "Accept"), |
| Parts = {chttpd:doc_etag(DDoc), Accept, {Name, Roles}}, |
| ETag = chttpd:make_etag({BaseSig, Parts}), |
| case chttpd:etag_match(Req, ETag) of |
| true -> throw({etag_match, ETag}); |
| false -> {ok, Acc0#lacc{etag=ETag}} |
| end |
| end, |
| Args = Args0#mrargs{preflight_fun=ETagFun}, |
| couch_httpd:etag_maybe(Req, fun() -> |
| couch_query_servers:with_ddoc_proc(DDoc, fun(QServer) -> |
| Acc = #lacc{db=Db, req=Req, qserver=QServer, lname=LName}, |
| case VName of |
| <<"_all_docs">> -> |
| couch_mrview:query_all_docs(Db, Args, fun list_cb/2, Acc); |
| _ -> |
| couch_mrview:query_view(Db, VDDoc, VName, Args, fun list_cb/2, Acc) |
| end |
| end) |
| end). |
| |
| |
| list_cb({meta, Meta}, #lacc{code=undefined} = Acc) -> |
| MetaProps = case couch_util:get_value(total, Meta) of |
| undefined -> []; |
| Total -> [{total_rows, Total}] |
| end ++ case couch_util:get_value(offset, Meta) of |
| undefined -> []; |
| Offset -> [{offset, Offset}] |
| end ++ case couch_util:get_value(update_seq, Meta) of |
| undefined -> []; |
| UpdateSeq -> [{update_seq, UpdateSeq}] |
| end, |
| start_list_resp({MetaProps}, Acc); |
| list_cb({row, Row}, #lacc{code=undefined} = Acc) -> |
| {ok, NewAcc} = start_list_resp({[]}, Acc), |
| send_list_row(Row, NewAcc); |
| list_cb({row, Row}, Acc) -> |
| send_list_row(Row, Acc); |
| list_cb(complete, Acc) -> |
| #lacc{qserver = {Proc, _}, resp = Resp0} = Acc, |
| if Resp0 =:= nil -> |
| {ok, #lacc{resp = Resp}} = start_list_resp({[]}, Acc); |
| true -> |
| Resp = Resp0 |
| end, |
| case couch_query_servers:proc_prompt(Proc, [<<"list_end">>]) of |
| [<<"end">>, Data, Headers] -> |
| Acc2 = fixup_headers(Headers, Acc#lacc{resp=Resp}), |
| #lacc{resp = Resp2} = send_non_empty_chunk(Acc2, Data); |
| [<<"end">>, Data] -> |
| #lacc{resp = Resp2} = send_non_empty_chunk(Acc#lacc{resp=Resp}, Data) |
| end, |
| last_chunk(Resp2), |
| {ok, Resp2}. |
| |
| start_list_resp(Head, Acc) -> |
| #lacc{db=Db, req=Req, qserver=QServer, lname=LName} = Acc, |
| JsonReq = json_req_obj(Req, Db), |
| |
| [<<"start">>,Chunk,JsonResp] = couch_query_servers:ddoc_proc_prompt(QServer, |
| [<<"lists">>, LName], [Head, JsonReq]), |
| Acc2 = send_non_empty_chunk(fixup_headers(JsonResp, Acc), Chunk), |
| {ok, Acc2}. |
| |
| fixup_headers(Headers, #lacc{etag=ETag} = Acc) -> |
| Headers2 = apply_etag(Headers, ETag), |
| #extern_resp_args{ |
| code = Code, |
| ctype = CType, |
| headers = ExtHeaders |
| } = chttpd_external:parse_external_response(Headers2), |
| Headers3 = chttpd_external:default_or_content_type(CType, ExtHeaders), |
| Acc#lacc{code=Code, headers=Headers3}. |
| |
| send_list_row(Row, #lacc{qserver = {Proc, _}, resp = Resp} = Acc) -> |
| RowObj = case couch_util:get_value(id, Row) of |
| undefined -> []; |
| Id -> [{id, Id}] |
| end ++ case couch_util:get_value(key, Row) of |
| undefined -> []; |
| Key -> [{key, Key}] |
| end ++ case couch_util:get_value(value, Row) of |
| undefined -> []; |
| Val -> [{value, Val}] |
| end ++ case couch_util:get_value(doc, Row) of |
| undefined -> []; |
| Doc -> [{doc, Doc}] |
| end, |
| try couch_query_servers:proc_prompt(Proc, [<<"list_row">>, {RowObj}]) of |
| [<<"chunks">>, Chunk, Headers] -> |
| Acc2 = send_non_empty_chunk(fixup_headers(Headers, Acc), Chunk), |
| {ok, Acc2}; |
| [<<"chunks">>, Chunk] -> |
| Acc2 = send_non_empty_chunk(Acc, Chunk), |
| {ok, Acc2}; |
| [<<"end">>, Chunk, Headers] -> |
| Acc2 = send_non_empty_chunk(fixup_headers(Headers, Acc), Chunk), |
| #lacc{resp = Resp2} = Acc2, |
| last_chunk(Resp2), |
| {stop, Acc2}; |
| [<<"end">>, Chunk] -> |
| Acc2 = send_non_empty_chunk(Acc, Chunk), |
| #lacc{resp = Resp2} = Acc2, |
| last_chunk(Resp2), |
| {stop, Acc2} |
| catch Error -> |
| case Resp of |
| undefined -> |
| {Code, _, _} = chttpd:error_info(Error), |
| #lacc{req=Req, headers=Headers} = Acc, |
| {ok, Resp2} = chttpd:start_chunked_response(Req, Code, Headers), |
| Acc2 = Acc#lacc{resp=Resp2, code=Code}; |
| _ -> Resp2 = Resp, Acc2 = Acc |
| end, |
| chttpd:send_chunked_error(Resp2, Error), |
| {stop, Acc2} |
| end. |
| |
| send_non_empty_chunk(Acc, []) -> |
| Acc; |
| send_non_empty_chunk(#lacc{resp=undefined} = Acc, Chunk) -> |
| #lacc{req=Req, code=Code, headers=Headers} = Acc, |
| {ok, Resp} = chttpd:start_chunked_response(Req, Code, Headers), |
| send_non_empty_chunk(Acc#lacc{resp = Resp}, Chunk); |
| send_non_empty_chunk(#lacc{resp=Resp} = Acc, Chunk) -> |
| chttpd:send_chunk(Resp, Chunk), |
| 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. |
| apply_headers(ExternalResponse, [ |
| {<<"Etag">>, CurrentEtag}, |
| {<<"Vary">>, <<"Accept">>} |
| ]). |
| |
| apply_headers(JsonResp, []) -> |
| JsonResp; |
| apply_headers(JsonResp, NewHeaders) -> |
| case couch_util:get_value(<<"headers">>, JsonResp) of |
| undefined -> |
| {[{<<"headers">>, {NewHeaders}}| JsonResp]}; |
| JsonHeaders -> |
| Headers = apply_headers1(JsonHeaders, NewHeaders), |
| NewKV = {<<"headers">>, Headers}, |
| {lists:keyreplace(<<"headers">>, 1, JsonResp, NewKV)} |
| end. |
| apply_headers1(JsonHeaders, [{Key, Value} | Rest]) -> |
| NewJsonHeaders = json_apply_field({Key, Value}, JsonHeaders), |
| apply_headers1(NewJsonHeaders, Rest); |
| apply_headers1(JsonHeaders, []) -> |
| JsonHeaders. |
| |
| |
| % Maybe this is in the proplists API |
| % todo move to couch_util |
| json_apply_field(H, {L}) -> |
| json_apply_field(H, L, []). |
| |
| |
| json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) -> |
| % drop matching keys |
| json_apply_field({Key, NewValue}, Headers, Acc); |
| json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) -> |
| % something else is next, leave it alone. |
| json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]); |
| json_apply_field({Key, NewValue}, [], Acc) -> |
| % end of list, add ours |
| {[{Key, NewValue}|Acc]}. |
| |
| |
| % This loads the db info if we have a fully loaded db record, but we might not |
| % have the db locally on this node, so then load the info through fabric. |
| json_req_obj(Req, #db{main_pid=Pid}=Db) when is_pid(Pid) -> |
| chttpd_external:json_req_obj(Req, Db); |
| json_req_obj(Req, Db) -> |
| % use a separate process because we're already in a receive loop, and |
| % json_req_obj calls fabric:get_db_info() |
| spawn_monitor(fun() -> exit(chttpd_external:json_req_obj(Req, Db)) end), |
| receive {'DOWN', _, _, _, JsonReq} -> JsonReq end. |
| |
| last_chunk(Resp) -> |
| chttpd:send_chunk(Resp, []). |
| |
| |
| -ifdef(TEST). |
| -include_lib("eunit/include/eunit.hrl"). |
| |
| apply_headers_test_() -> |
| [ |
| should_apply_headers(), |
| should_apply_headers_with_merge(), |
| should_apply_headers_with_merge_overwrite() |
| ]. |
| |
| should_apply_headers() -> |
| ?_test(begin |
| JsonResp = [{<<"code">>, 201}], |
| Headers = [{<<"foo">>, <<"bar">>}], |
| {Props} = apply_headers(JsonResp, Headers), |
| JsonHeaders = couch_util:get_value(<<"headers">>, Props), |
| ?assertEqual({Headers}, JsonHeaders) |
| end). |
| |
| should_apply_headers_with_merge() -> |
| ?_test(begin |
| BaseHeaders = [{<<"bar">>, <<"baz">>}], |
| NewHeaders = [{<<"foo">>, <<"bar">>}], |
| JsonResp = [ |
| {<<"code">>, 201}, |
| {<<"headers">>, {BaseHeaders}} |
| ], |
| {Props} = apply_headers(JsonResp, NewHeaders), |
| JsonHeaders = couch_util:get_value(<<"headers">>, Props), |
| ExpectedHeaders = {NewHeaders ++ BaseHeaders}, |
| ?assertEqual(ExpectedHeaders, JsonHeaders) |
| end). |
| |
| should_apply_headers_with_merge_overwrite() -> |
| ?_test(begin |
| BaseHeaders = [{<<"foo">>, <<"bar">>}], |
| NewHeaders = [{<<"foo">>, <<"baz">>}], |
| JsonResp = [ |
| {<<"code">>, 201}, |
| {<<"headers">>, {BaseHeaders}} |
| ], |
| {Props} = apply_headers(JsonResp, NewHeaders), |
| JsonHeaders = couch_util:get_value(<<"headers">>, Props), |
| ?assertEqual({NewHeaders}, JsonHeaders) |
| end). |
| |
| -endif. |