| % 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_httpd_show). |
| |
| -export([handle_doc_show_req/2, handle_doc_update_req/2, handle_view_list_req/2, |
| handle_doc_show/5, handle_view_list/7]). |
| |
| -include("couch_db.hrl"). |
| |
| -import(couch_httpd, |
| [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, |
| start_json_response/2,send_chunk/2,send_chunked_error/2, |
| start_chunked_response/3, send_error/4]). |
| |
| handle_doc_show_req(#httpd{ |
| method='GET', |
| path_parts=[_DbName, _Design, DesignName, _Show, ShowName, DocId] |
| }=Req, Db) -> |
| handle_doc_show(Req, DesignName, ShowName, DocId, Db); |
| |
| handle_doc_show_req(#httpd{ |
| path_parts=[_DbName, _Design, DesignName, _Show, ShowName] |
| }=Req, Db) -> |
| handle_doc_show(Req, DesignName, ShowName, nil, Db); |
| |
| handle_doc_show_req(#httpd{method='GET'}=Req, _Db) -> |
| send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>); |
| |
| handle_doc_show_req(Req, _Db) -> |
| send_method_not_allowed(Req, "GET,POST,HEAD"). |
| |
| handle_doc_update_req(#httpd{ |
| method = 'PUT', |
| path_parts=[_DbName, _Design, DesignName, _Update, UpdateName, DocId] |
| }=Req, Db) -> |
| DesignId = <<"_design/", DesignName/binary>>, |
| #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), |
| Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), |
| UpdateSrc = couch_util:get_nested_json_value({Props}, [<<"updates">>, UpdateName]), |
| Doc = try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of |
| FoundDoc -> FoundDoc |
| catch |
| _ -> nil |
| end, |
| send_doc_update_response(Lang, UpdateSrc, DocId, Doc, Req, Db); |
| |
| handle_doc_update_req(#httpd{ |
| method = 'POST', |
| path_parts=[_DbName, _Design, DesignName, _Update, UpdateName] |
| }=Req, Db) -> |
| DesignId = <<"_design/", DesignName/binary>>, |
| #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), |
| Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), |
| UpdateSrc = couch_util:get_nested_json_value({Props}, [<<"updates">>, UpdateName]), |
| send_doc_update_response(Lang, UpdateSrc, nil, nil, Req, Db); |
| |
| handle_doc_update_req(#httpd{ |
| path_parts=[_DbName, _Design, _DesignName, _Update, _UpdateName, _DocId] |
| }=Req, _Db) -> |
| send_method_not_allowed(Req, "PUT"); |
| |
| handle_doc_update_req(#httpd{ |
| path_parts=[_DbName, _Design, _DesignName, _Update, _UpdateName] |
| }=Req, _Db) -> |
| send_method_not_allowed(Req, "POST"); |
| |
| handle_doc_update_req(Req, _Db) -> |
| send_error(Req, 404, <<"update_error">>, <<"Invalid path.">>). |
| |
| |
| |
| |
| handle_doc_show(Req, DesignName, ShowName, DocId, Db) -> |
| DesignId = <<"_design/", DesignName/binary>>, |
| #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, nil, []), |
| Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>), |
| ShowSrc = couch_util:get_nested_json_value({Props}, [<<"shows">>, ShowName]), |
| Doc = case DocId of |
| nil -> nil; |
| _ -> |
| try couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of |
| FoundDoc -> FoundDoc |
| catch |
| _ -> nil |
| end |
| end, |
| send_doc_show_response(Lang, ShowSrc, DocId, Doc, Req, Db). |
| |
| % view-list request with view and list from same design doc. |
| handle_view_list_req(#httpd{method='GET', |
| path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewName]}=Req, Db) -> |
| handle_view_list(Req, DesignName, ListName, DesignName, ViewName, Db, nil); |
| |
| % view-list request with view and list from different design docs. |
| handle_view_list_req(#httpd{method='GET', |
| path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewDesignName, ViewName]}=Req, Db) -> |
| handle_view_list(Req, DesignName, ListName, ViewDesignName, ViewName, Db, nil); |
| |
| handle_view_list_req(#httpd{method='GET'}=Req, _Db) -> |
| send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>); |
| |
| handle_view_list_req(#httpd{method='POST', |
| path_parts=[_DbName, _Design, DesignName, _List, ListName, ViewName]}=Req, Db) -> |
| ReqBody = couch_httpd:body(Req), |
| {Props2} = ?JSON_DECODE(ReqBody), |
| Keys = proplists:get_value(<<"keys">>, Props2, nil), |
| handle_view_list(Req#httpd{req_body=ReqBody}, DesignName, ListName, DesignName, ViewName, Db, Keys); |
| |
| handle_view_list_req(Req, _Db) -> |
| send_method_not_allowed(Req, "GET,POST,HEAD"). |
| |
| handle_view_list(Req, ListDesignName, ListName, ViewDesignName, ViewName, Db, Keys) -> |
| ListDesignId = <<"_design/", ListDesignName/binary>>, |
| #doc{body={ListProps}} = couch_httpd_db:couch_doc_open(Db, ListDesignId, nil, []), |
| if |
| ViewDesignName == ListDesignName -> |
| ViewDesignId = ListDesignId; |
| true -> |
| ViewDesignId = <<"_design/", ViewDesignName/binary>> |
| end, |
| |
| ListLang = proplists:get_value(<<"language">>, ListProps, <<"javascript">>), |
| ListSrc = couch_util:get_nested_json_value({ListProps}, [<<"lists">>, ListName]), |
| send_view_list_response(ListLang, ListSrc, ViewName, ViewDesignId, Req, Db, Keys). |
| |
| |
| send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Keys) -> |
| Stale = couch_httpd_view:get_stale_type(Req), |
| Reduce = couch_httpd_view:get_reduce_type(Req), |
| case couch_view:get_map_view(Db, DesignId, ViewName, Stale) of |
| {ok, View, Group} -> |
| QueryArgs = couch_httpd_view:parse_view_params(Req, Keys, map), |
| output_map_list(Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys); |
| {not_found, _Reason} -> |
| case couch_view:get_reduce_view(Db, DesignId, ViewName, Stale) of |
| {ok, ReduceView, Group} -> |
| case Reduce of |
| false -> |
| QueryArgs = couch_httpd_view:parse_view_params( |
| Req, Keys, map_red |
| ), |
| MapView = couch_view:extract_map_view(ReduceView), |
| output_map_list(Req, Lang, ListSrc, MapView, Group, Db, QueryArgs, Keys); |
| _ -> |
| QueryArgs = couch_httpd_view:parse_view_params( |
| Req, Keys, reduce |
| ), |
| output_reduce_list(Req, Lang, ListSrc, ReduceView, Group, Db, QueryArgs, Keys) |
| end; |
| {not_found, Reason} -> |
| throw({not_found, Reason}) |
| end |
| end. |
| |
| |
| output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) -> |
| #view_query_args{ |
| limit = Limit, |
| direction = Dir, |
| skip = SkipCount, |
| start_key = StartKey, |
| start_docid = StartDocId |
| } = QueryArgs, |
| {ok, RowCount} = couch_view:get_row_count(View), |
| Start = {StartKey, StartDocId}, |
| Headers = MReq:get(headers), |
| Hlist = mochiweb_headers:to_list(Headers), |
| Accept = proplists:get_value('Accept', Hlist), |
| CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| % get the os process here |
| % pass it into the view fold with closures |
| {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), |
| |
| StartListRespFun = make_map_start_resp_fun(QueryServer, Db), |
| SendListRowFun = make_map_send_row_fun(QueryServer), |
| |
| FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, RowCount, |
| #view_fold_helper_funs{ |
| reduce_count = fun couch_view:reduce_to_count/1, |
| start_response = StartListRespFun, |
| send_row = SendListRowFun |
| }), |
| FoldAccInit = {Limit, SkipCount, undefined, [], nil}, |
| {ok, FoldResult} = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit), |
| finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) |
| end); |
| |
| output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) -> |
| #view_query_args{ |
| limit = Limit, |
| direction = Dir, |
| skip = SkipCount, |
| start_docid = StartDocId |
| } = QueryArgs, |
| {ok, RowCount} = couch_view:get_row_count(View), |
| Headers = MReq:get(headers), |
| Hlist = mochiweb_headers:to_list(Headers), |
| Accept = proplists:get_value('Accept', Hlist), |
| CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| % get the os process here |
| % pass it into the view fold with closures |
| {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), |
| |
| StartListRespFun = make_map_start_resp_fun(QueryServer, Db), |
| SendListRowFun = make_map_send_row_fun(QueryServer), |
| |
| FoldAccInit = {Limit, SkipCount, undefined, [], nil}, |
| {ok, FoldResult} = lists:foldl( |
| fun(Key, {ok, FoldAcc}) -> |
| FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs#view_query_args{ |
| start_key = Key, |
| end_key = Key |
| }, CurrentEtag, Db, RowCount, |
| #view_fold_helper_funs{ |
| reduce_count = fun couch_view:reduce_to_count/1, |
| start_response = StartListRespFun, |
| send_row = SendListRowFun |
| }), |
| couch_view:fold(View, {Key, StartDocId}, Dir, FoldlFun, FoldAcc) |
| end, {ok, FoldAccInit}, Keys), |
| finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount) |
| end). |
| |
| make_map_start_resp_fun(QueryServer, Db) -> |
| fun(Req, Etag, TotalRows, Offset, _Acc) -> |
| Head = {[{<<"total_rows">>, TotalRows}, {<<"offset">>, Offset}]}, |
| start_list_resp(QueryServer, Req, Db, Head, Etag) |
| end. |
| |
| make_reduce_start_resp_fun(QueryServer, _Req, Db, _CurrentEtag) -> |
| fun(Req2, Etag, _Acc) -> |
| start_list_resp(QueryServer, Req2, Db, {[]}, Etag) |
| end. |
| |
| start_list_resp(QueryServer, Req, Db, Head, Etag) -> |
| [<<"start">>,Chunks,JsonResp] = couch_query_servers:render_list_head(QueryServer, |
| Req, Db, Head), |
| JsonResp2 = apply_etag(JsonResp, Etag), |
| #extern_resp_args{ |
| code = Code, |
| ctype = CType, |
| headers = ExtHeaders |
| } = couch_httpd_external:parse_external_response(JsonResp2), |
| JsonHeaders = couch_httpd_external:default_or_content_type(CType, ExtHeaders), |
| {ok, Resp} = start_chunked_response(Req, Code, JsonHeaders), |
| {ok, Resp, ?b2l(?l2b(Chunks))}. |
| |
| make_map_send_row_fun(QueryServer) -> |
| fun(Resp, Db, Row, IncludeDocs, RowFront) -> |
| send_list_row(Resp, QueryServer, Db, Row, RowFront, IncludeDocs) |
| end. |
| |
| make_reduce_send_row_fun(QueryServer, Db) -> |
| fun(Resp, Row, RowFront) -> |
| send_list_row(Resp, QueryServer, Db, Row, RowFront, false) |
| end. |
| |
| send_list_row(Resp, QueryServer, Db, Row, RowFront, IncludeDoc) -> |
| try |
| [Go,Chunks] = couch_query_servers:render_list_row(QueryServer, Db, Row, IncludeDoc), |
| Chunk = RowFront ++ ?b2l(?l2b(Chunks)), |
| send_non_empty_chunk(Resp, Chunk), |
| case Go of |
| <<"chunks">> -> |
| {ok, ""}; |
| <<"end">> -> |
| {stop, stop} |
| end |
| catch |
| throw:Error -> |
| send_chunked_error(Resp, Error), |
| throw({already_sent, Resp, Error}) |
| end. |
| |
| send_non_empty_chunk(Resp, Chunk) -> |
| case Chunk of |
| [] -> ok; |
| _ -> send_chunk(Resp, Chunk) |
| end. |
| |
| output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) -> |
| #view_query_args{ |
| limit = Limit, |
| direction = Dir, |
| skip = SkipCount, |
| start_key = StartKey, |
| start_docid = StartDocId, |
| end_key = EndKey, |
| end_docid = EndDocId, |
| group_level = GroupLevel |
| } = QueryArgs, |
| Headers = MReq:get(headers), |
| Hlist = mochiweb_headers:to_list(Headers), |
| Accept = proplists:get_value('Accept', Hlist), |
| CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| % get the os process here |
| % pass it into the view fold with closures |
| {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), |
| StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag), |
| SendListRowFun = make_reduce_send_row_fun(QueryServer, Db), |
| |
| {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, |
| GroupLevel, QueryArgs, CurrentEtag, |
| #reduce_fold_helper_funs{ |
| start_response = StartListRespFun, |
| send_row = SendListRowFun |
| }), |
| FoldAccInit = {Limit, SkipCount, undefined, []}, |
| {ok, FoldResult} = couch_view:fold_reduce(View, Dir, {StartKey, StartDocId}, |
| {EndKey, EndDocId}, GroupRowsFun, RespFun, |
| FoldAccInit), |
| finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) |
| end); |
| |
| output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) -> |
| #view_query_args{ |
| limit = Limit, |
| direction = Dir, |
| skip = SkipCount, |
| start_docid = StartDocId, |
| end_docid = EndDocId, |
| group_level = GroupLevel |
| } = QueryArgs, |
| Headers = MReq:get(headers), |
| Hlist = mochiweb_headers:to_list(Headers), |
| Accept = proplists:get_value('Accept', Hlist), |
| CurrentEtag = couch_httpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx, Keys}), |
| |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| % get the os process here |
| % pass it into the view fold with closures |
| {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), |
| StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag), |
| SendListRowFun = make_reduce_send_row_fun(QueryServer, Db), |
| |
| {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req, |
| GroupLevel, QueryArgs, CurrentEtag, |
| #reduce_fold_helper_funs{ |
| start_response = StartListRespFun, |
| send_row = SendListRowFun |
| }), |
| FoldAccInit = {Limit, SkipCount, undefined, []}, |
| {ok, FoldResult} = lists:foldl( |
| fun(Key, {ok, FoldAcc}) -> |
| couch_view:fold_reduce(View, Dir, {Key, StartDocId}, |
| {Key, EndDocId}, GroupRowsFun, RespFun, FoldAcc) |
| end, {ok, FoldAccInit}, Keys), |
| finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null) |
| end). |
| |
| finish_list(Req, QueryServer, Etag, FoldResult, StartFun, TotalRows) -> |
| FoldResult2 = case FoldResult of |
| {Limit, SkipCount, Response, RowAcc} -> |
| {Limit, SkipCount, Response, RowAcc, nil}; |
| Else -> |
| Else |
| end, |
| case FoldResult2 of |
| {_, _, undefined, _, _} -> |
| {ok, Resp, BeginBody} = |
| render_head_for_empty_list(StartFun, Req, Etag, TotalRows), |
| [<<"end">>, Chunks] = couch_query_servers:render_list_tail(QueryServer), |
| Chunk = BeginBody ++ ?b2l(?l2b(Chunks)), |
| send_non_empty_chunk(Resp, Chunk); |
| {_, _, Resp, stop, _} -> |
| ok; |
| {_, _, Resp, _, _} -> |
| [<<"end">>, Chunks] = couch_query_servers:render_list_tail(QueryServer), |
| send_non_empty_chunk(Resp, ?b2l(?l2b(Chunks))) |
| end, |
| couch_query_servers:stop_doc_map(QueryServer), |
| send_chunk(Resp, []). |
| |
| |
| render_head_for_empty_list(StartListRespFun, Req, Etag, null) -> |
| StartListRespFun(Req, Etag, []); % for reduce |
| render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows) -> |
| StartListRespFun(Req, Etag, TotalRows, null, []). |
| |
| send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq, user_ctx=UserCtx}=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, UserCtx}), |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, |
| DocId, nil, Req, Db), |
| JsonResp = apply_etag(ExternalResp, CurrentEtag), |
| couch_httpd_external:send_external_response(Req, JsonResp) |
| end); |
| |
| send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) -> |
| % calculate the etag |
| Headers = MReq:get(headers), |
| Hlist = mochiweb_headers:to_list(Headers), |
| Accept = proplists:get_value('Accept', Hlist), |
| CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept, UserCtx}), |
| % We know our etag now |
| couch_httpd:etag_respond(Req, CurrentEtag, fun() -> |
| [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, |
| DocId, Doc, Req, Db), |
| JsonResp = apply_etag(ExternalResp, CurrentEtag), |
| couch_httpd_external:send_external_response(Req, JsonResp) |
| end). |
| |
| send_doc_update_response(Lang, UpdateSrc, DocId, Doc, Req, Db) -> |
| case couch_query_servers:render_doc_update(Lang, UpdateSrc, |
| DocId, Doc, Req, Db) of |
| [<<"up">>, {NewJsonDoc}, JsonResp] -> |
| Options = case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of |
| "true" -> |
| [full_commit]; |
| _ -> |
| [] |
| end, |
| NewDoc = couch_doc:from_json_obj({NewJsonDoc}), |
| Code = 201, |
| % todo set location field |
| {ok, _NewRev} = couch_db:update_doc(Db, NewDoc, Options); |
| [<<"up">>, _Other, JsonResp] -> |
| Code = 200, |
| ok |
| end, |
| JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp), |
| couch_httpd_external:send_external_response(Req, JsonResp2). |
| |
| % 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]}. |
| |
| 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 = json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders), |
| JsonHeadersVaried = json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged), |
| {<<"headers">>, JsonHeadersVaried}; |
| _ -> % skip non-header fields |
| Field |
| end || Field <- ExternalResponse]} |
| end. |
| |