summaryrefslogtreecommitdiff
path: root/apps/chttpd/src/chttpd_show.erl
diff options
context:
space:
mode:
Diffstat (limited to 'apps/chttpd/src/chttpd_show.erl')
-rw-r--r--apps/chttpd/src/chttpd_show.erl311
1 files changed, 311 insertions, 0 deletions
diff --git a/apps/chttpd/src/chttpd_show.erl b/apps/chttpd/src/chttpd_show.erl
new file mode 100644
index 00000000..fba7f040
--- /dev/null
+++ b/apps/chttpd/src/chttpd_show.erl
@@ -0,0 +1,311 @@
+% 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/3, handle_doc_update_req/3, handle_view_list_req/3]).
+
+-include_lib("couch/include/couch_db.hrl").
+
+-import(chttpd,
+ [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
+ start_json_response/2,send_chunk/2,last_chunk/1,send_chunked_error/2,
+ start_chunked_response/3, send_error/4]).
+
+-record(lacc, {
+ req,
+ resp = nil,
+ qserver,
+ lname,
+ db,
+ etag
+}).
+
+% /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 fabric:open_doc(Db, DocId, [conflicts]) of
+ {ok, 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) ->
+ 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,
+ couch_httpd:make_etag({couch_httpd:doc_etag(DDoc), DocPart, Accept,
+ UserCtx#user_ctx.roles, More}).
+
+% /db/_design/foo/update/bar/docid
+% 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");
+
+handle_doc_update_req(#httpd{
+ path_parts=[_, _, _, _, UpdateName, DocId]
+ }=Req, Db, DDoc) ->
+ Doc = maybe_open_doc(Db, DocId),
+ send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId);
+
+handle_doc_update_req(#httpd{
+ path_parts=[_, _, _, _, UpdateName]
+ }=Req, Db, DDoc) ->
+ send_doc_update_response(Req, Db, DDoc, UpdateName, nil, null);
+
+handle_doc_update_req(Req, _Db, _DDoc) ->
+ 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],
+ case couch_query_servers:ddoc_prompt(DDoc, Cmd, [JsonDoc, JsonReq]) of
+ [<<"up">>, {NewJsonDoc}, JsonResp] ->
+ 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}),
+ Code = 201,
+ {ok, _NewRev} = fabric:update_doc(Db, NewDoc, Options);
+ [<<"up">>, _Other, JsonResp] ->
+ Code = 200
+ end,
+ JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp),
+ % todo set location field
+ chttpd_external:send_external_response(Req, JsonResp2).
+
+
+% view-list request with view and list from same design doc.
+handle_view_list_req(#httpd{method='GET',
+ path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) ->
+ handle_view_list(Req, Db, DDoc, ListName, {DesignName, ViewName}, nil);
+
+% view-list request with view and list from different design docs.
+handle_view_list_req(#httpd{method='GET',
+ path_parts=[_, _, _, _, ListName, DesignName, ViewName]}=Req, Db, DDoc) ->
+ handle_view_list(Req, Db, DDoc, ListName, {DesignName, ViewName}, nil);
+
+handle_view_list_req(#httpd{method='GET'}=Req, _Db, _DDoc) ->
+ send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>);
+
+handle_view_list_req(#httpd{method='POST',
+ path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) ->
+ ReqBody = couch_httpd:body(Req),
+ {Props2} = ?JSON_DECODE(ReqBody),
+ Keys = proplists:get_value(<<"keys">>, Props2, nil),
+ handle_view_list(Req#httpd{req_body=ReqBody}, Db, DDoc, ListName,
+ {DesignName, ViewName}, Keys);
+
+handle_view_list_req(#httpd{method='POST',
+ path_parts=[_, _, _, _, ListName, DesignName, ViewName]}=Req, Db, DDoc) ->
+ ReqBody = couch_httpd:body(Req),
+ {Props2} = ?JSON_DECODE(ReqBody),
+ Keys = proplists:get_value(<<"keys">>, Props2, nil),
+ handle_view_list(Req#httpd{req_body=ReqBody}, Db, DDoc, ListName,
+ {DesignName, ViewName}, Keys);
+
+handle_view_list_req(#httpd{method='POST'}=Req, _Db, _DDoc) ->
+ send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>);
+
+handle_view_list_req(Req, _Db, _DDoc) ->
+ send_method_not_allowed(Req, "GET,POST,HEAD").
+
+handle_view_list(Req, Db, DDoc, LName, {ViewDesignName, ViewName}, Keys) ->
+ {ok, VDoc} = fabric:open_doc(Db, <<"_design/", ViewDesignName/binary>>, []),
+ Group = couch_view_group:design_doc_to_view_group(Db, VDoc),
+ 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),
+ CB = fun list_callback/2,
+ Etag = couch_uuids:new(),
+ chttpd:etag_respond(Req, Etag, fun() ->
+ couch_query_servers:with_ddoc_proc(DDoc, fun(QServer) ->
+ Acc0 = #lacc{
+ lname = LName,
+ req = Req,
+ qserver = QServer,
+ db = Db,
+ etag = Etag
+ },
+ fabric:query_view(Db, VDoc, ViewName, CB, Acc0, QueryArgs)
+ end)
+ end).
+
+list_callback({total_and_offset, Total, Offset}, #lacc{resp=nil} = Acc) ->
+ start_list_resp({[{<<"total_rows">>, Total}, {<<"offset">>, Offset}]}, Acc);
+list_callback({total_and_offset, _, _}, Acc) ->
+ % a sorted=false view where the message came in late. Ignore.
+ {ok, Acc};
+list_callback({row, Row}, #lacc{resp=nil} = Acc) ->
+ % first row of a reduce view, or a sorted=false view
+ {ok, NewAcc} = start_list_resp({[]}, Acc),
+ send_list_row(Row, NewAcc);
+list_callback({row, Row}, Acc) ->
+ send_list_row(Row, Acc);
+list_callback(complete, Acc) ->
+ #lacc{qserver = {Proc, _}, resp = Resp0} = Acc,
+ if Resp0 =:= nil ->
+ {ok, #lacc{resp = Resp}} = start_list_resp({[]}, Acc);
+ true ->
+ Resp = Resp0
+ end,
+ [<<"end">>, Chunk] = couch_query_servers:proc_prompt(Proc, [<<"list_end">>]),
+ send_non_empty_chunk(Resp, Chunk),
+ couch_httpd:last_chunk(Resp),
+ {ok, Resp};
+list_callback({error, Reason}, {_, Resp}) ->
+ chttpd:send_chunked_error(Resp, {error, Reason}).
+
+start_list_resp(Head, Acc) ->
+ #lacc{
+ req = Req,
+ db = Db,
+ qserver = QServer,
+ lname = LName,
+ etag = Etag
+ } = Acc,
+
+ % 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} -> ok end,
+
+ [<<"start">>,Chunk,JsonResp] = couch_query_servers:ddoc_proc_prompt(QServer,
+ [<<"lists">>, LName], [Head, JsonReq]),
+ 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),
+ send_non_empty_chunk(Resp, Chunk),
+ {ok, Acc#lacc{resp=Resp}}.
+
+send_list_row(Row, #lacc{qserver = {Proc, _}, resp = Resp} = Acc) ->
+ try couch_query_servers:proc_prompt(Proc, [<<"list_row">>, Row]) of
+ [<<"chunks">>, Chunk] ->
+ send_non_empty_chunk(Resp, Chunk),
+ {ok, Acc};
+ [<<"end">>, Chunk] ->
+ send_non_empty_chunk(Resp, Chunk),
+ couch_httpd:last_chunk(Resp),
+ {stop, Resp}
+ catch Error ->
+ chttpd:send_chunked_error(Resp, Error),
+ {stop, Resp}
+ end.
+
+send_non_empty_chunk(_, []) ->
+ ok;
+send_non_empty_chunk(Resp, Chunk) ->
+ send_chunk(Resp, Chunk).
+
+% 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.
+