diff options
Diffstat (limited to 'src/chttpd_show.erl')
-rw-r--r-- | src/chttpd_show.erl | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/src/chttpd_show.erl b/src/chttpd_show.erl new file mode 100644 index 00000000..bba05ec1 --- /dev/null +++ b/src/chttpd_show.erl @@ -0,0 +1,496 @@ +% 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(chttpd_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, start_list_resp/5, + send_list_row/6]). + +-include("chttpd.hrl"). + +-import(chttpd, + [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}} = chttpd_db:couch_doc_open(Db, DesignId, nil, []), + Lang = couch_util:get_value(<<"language">>, Props, <<"javascript">>), + UpdateSrc = couch_util:get_nested_json_value({Props}, [<<"updates">>, UpdateName]), + Doc = try chttpd_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}} = chttpd_db:couch_doc_open(Db, DesignId, nil, []), + Lang = couch_util: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}} = chttpd_db:couch_doc_open(Db, DesignId, nil, []), + Lang = couch_util:get_value(<<"language">>, Props, <<"javascript">>), + ShowSrc = couch_util:get_nested_json_value({Props}, [<<"shows">>, ShowName]), + Doc = case DocId of + nil -> nil; + _ -> + try chttpd_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 = chttpd:body(Req), + {Props2} = ?JSON_DECODE(ReqBody), + Keys = couch_util: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}} = chttpd_db:couch_doc_open(Db, ListDesignId, nil, []), + if + ViewDesignName == ListDesignName -> + ViewProps = ListProps, + ViewDesignId = ListDesignId; + true -> + ViewDesignId = <<"_design/", ViewDesignName/binary>>, + #doc{body={ViewProps}} = chttpd_db:couch_doc_open(Db, ViewDesignId, nil, []) + end, + + ViewLang = couch_util:get_value(<<"language">>, ViewProps, <<"javascript">>), + ListSrc = couch_util:get_nested_json_value({ListProps}, [<<"lists">>, ListName]), + Group = couch_view_group:design_doc_to_view_group(Db, #doc{id=ViewDesignId, + body={ViewProps}}), + send_view_list_response(ViewLang, ListSrc, ViewName, ViewDesignId, Req, Db, + Group, Keys). + % send_view_list_response(ViewLang, ListSrc, ViewName, ViewDesignId, Req, Db, Keys). + + +send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Group, Keys) -> + IsReduce = chttpd_view:get_reduce_type(Req), + ViewType = chttpd_view:extract_view_type(ViewName, Group#group.views, + IsReduce), + QueryArgs = chttpd_view:parse_view_params(Req, Keys, ViewType), + {ok, QueryServer} = couch_query_servers:start_view_list(Lang, ListSrc), + StartListRespFun = make_map_start_resp_fun(QueryServer, Db), + Etag = couch_util:new_uuid(), + chttpd:etag_respond(Req, Etag, fun() -> + {ok, Total, Result} = ?COUCH:list_view(Req, Db, DesignId, ViewName, + Keys, QueryArgs, QueryServer), + finish_list(Req, QueryServer, Etag, Result, StartListRespFun, Total) + end). + +send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Keys) -> + Stale = chttpd_view:get_stale_type(Req), + Reduce = chttpd_view:get_reduce_type(Req), + case ?COUCH:get_map_view(Db, DesignId, ViewName, Stale) of + {ok, View, Group} -> + QueryArgs = chttpd_view:parse_view_params(Req, Keys, map), + output_map_list(Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys); + {not_found, _Reason} -> + case ?COUCH:get_reduce_view(Db, DesignId, ViewName, Stale) of + {ok, ReduceView, Group} -> + case Reduce of + false -> + QueryArgs = chttpd_view:parse_view_params( + Req, Keys, map_red + ), + MapView = ?COUCH:extract_map_view(ReduceView), + output_map_list(Req, Lang, ListSrc, MapView, Group, Db, QueryArgs, Keys); + _ -> + QueryArgs = chttpd_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:get_row_count(View), + Start = {StartKey, StartDocId}, + Headers = MReq:get(headers), + Hlist = mochiweb_headers:to_list(Headers), + Accept = couch_util:get_value('Accept', Hlist), + CurrentEtag = chttpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), + chttpd: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 = chttpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, RowCount, + #view_fold_helper_funs{ + reduce_count = fun ?COUCH: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:get_row_count(View), + Headers = MReq:get(headers), + Hlist = mochiweb_headers:to_list(Headers), + Accept = couch_util:get_value('Accept', Hlist), + CurrentEtag = chttpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), + chttpd: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 = chttpd_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: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 + } = chttpd_external:parse_external_response(JsonResp2), + JsonHeaders = chttpd_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 = couch_util:get_value('Accept', Hlist), + CurrentEtag = chttpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx}), + chttpd: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} = chttpd_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 = couch_util:get_value('Accept', Hlist), + CurrentEtag = chttpd_view:view_group_etag(Group, Db, {Lang, ListSrc, Accept, UserCtx, Keys}), + + chttpd: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} = chttpd_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 = couch_util:get_value('Accept', Hlist), + CurrentEtag = chttpd:make_etag({Lang, ShowSrc, nil, Accept, UserCtx}), + chttpd:etag_respond(Req, CurrentEtag, fun() -> + [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, + DocId, nil, Req, Db), + JsonResp = apply_etag(ExternalResp, CurrentEtag), + chttpd_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 = couch_util:get_value('Accept', Hlist), + CurrentEtag = chttpd:make_etag({Lang, ShowSrc, Revs, Accept, UserCtx}), + % We know our etag now + chttpd:etag_respond(Req, CurrentEtag, fun() -> + [<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc, + DocId, Doc, Req, Db), + JsonResp = apply_etag(ExternalResp, CurrentEtag), + chttpd_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 chttpd: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:update_doc(Db, NewDoc, Options); + [<<"up">>, _Other, JsonResp] -> + Code = 200, + ok + end, + JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp), + chttpd_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 couch_util: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. + |