PageRenderTime 25ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/apps/couch/src/couch_httpd_show.erl

http://github.com/cloudant/bigcouch
Erlang | 404 lines | 299 code | 64 blank | 41 comment | 1 complexity | 688cf754556ae99f3b4d2678d603da0a MD5 | raw file
Possible License(s): Apache-2.0
  1. % Licensed under the Apache License, Version 2.0 (the "License"); you may not
  2. % use this file except in compliance with the License. You may obtain a copy of
  3. % the License at
  4. %
  5. % http://www.apache.org/licenses/LICENSE-2.0
  6. %
  7. % Unless required by applicable law or agreed to in writing, software
  8. % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. % License for the specific language governing permissions and limitations under
  11. % the License.
  12. -module(couch_httpd_show).
  13. -export([handle_doc_show_req/3, handle_doc_update_req/3, handle_view_list_req/3,
  14. handle_view_list/6, get_fun_key/3]).
  15. -include("couch_db.hrl").
  16. -import(couch_httpd,
  17. [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
  18. start_json_response/2,send_chunk/2,last_chunk/1,send_chunked_error/2,
  19. start_chunked_response/3, send_error/4]).
  20. % /db/_design/foo/_show/bar/docid
  21. % show converts a json doc to a response of any content-type.
  22. % it looks up the doc an then passes it to the query server.
  23. % then it sends the response from the query server to the http client.
  24. maybe_open_doc(Db, DocId) ->
  25. case catch couch_httpd_db:couch_doc_open(Db, DocId, nil, [conflicts]) of
  26. {not_found, missing} -> nil;
  27. {not_found,deleted} -> nil;
  28. Doc -> Doc
  29. end.
  30. handle_doc_show_req(#httpd{
  31. path_parts=[_, _, _, _, ShowName, DocId]
  32. }=Req, Db, DDoc) ->
  33. % open the doc
  34. Doc = maybe_open_doc(Db, DocId),
  35. % we don't handle revs here b/c they are an internal api
  36. % returns 404 if there is no doc with DocId
  37. handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId);
  38. handle_doc_show_req(#httpd{
  39. path_parts=[_, _, _, _, ShowName, DocId|Rest]
  40. }=Req, Db, DDoc) ->
  41. DocParts = [DocId|Rest],
  42. DocId1 = ?l2b(string:join([?b2l(P)|| P <- DocParts], "/")),
  43. % open the doc
  44. Doc = maybe_open_doc(Db, DocId1),
  45. % we don't handle revs here b/c they are an internal api
  46. % pass 404 docs to the show function
  47. handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId1);
  48. handle_doc_show_req(#httpd{
  49. path_parts=[_, _, _, _, ShowName]
  50. }=Req, Db, DDoc) ->
  51. % with no docid the doc is nil
  52. handle_doc_show(Req, Db, DDoc, ShowName, nil);
  53. handle_doc_show_req(Req, _Db, _DDoc) ->
  54. send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>).
  55. handle_doc_show(Req, Db, DDoc, ShowName, Doc) ->
  56. handle_doc_show(Req, Db, DDoc, ShowName, Doc, null).
  57. handle_doc_show(Req, Db, DDoc, ShowName, Doc, DocId) ->
  58. % get responder for ddoc/showname
  59. CurrentEtag = show_etag(Req, Doc, DDoc, []),
  60. couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
  61. JsonReq = couch_httpd_external:json_req_obj(Req, Db, DocId),
  62. JsonDoc = couch_query_servers:json_doc(Doc),
  63. [<<"resp">>, ExternalResp] =
  64. couch_query_servers:ddoc_prompt(DDoc, [<<"shows">>, ShowName], [JsonDoc, JsonReq]),
  65. JsonResp = apply_etag(ExternalResp, CurrentEtag),
  66. couch_httpd_external:send_external_response(Req, JsonResp)
  67. end).
  68. show_etag(#httpd{user_ctx=UserCtx}=Req, Doc, DDoc, More) ->
  69. Accept = couch_httpd:header_value(Req, "Accept"),
  70. DocPart = case Doc of
  71. nil -> nil;
  72. Doc -> couch_httpd:doc_etag(Doc)
  73. end,
  74. couch_httpd:make_etag({couch_httpd:doc_etag(DDoc), DocPart, Accept, UserCtx#user_ctx.roles, More}).
  75. get_fun_key(DDoc, Type, Name) ->
  76. #doc{body={Props}} = DDoc,
  77. Lang = couch_util:get_value(<<"language">>, Props, <<"javascript">>),
  78. Src = couch_util:get_nested_json_value({Props}, [Type, Name]),
  79. {Lang, Src}.
  80. % /db/_design/foo/update/bar/docid
  81. % updates a doc based on a request
  82. % handle_doc_update_req(#httpd{method = 'GET'}=Req, _Db, _DDoc) ->
  83. % % anything but GET
  84. % send_method_not_allowed(Req, "POST,PUT,DELETE,ETC");
  85. handle_doc_update_req(#httpd{
  86. path_parts=[_, _, _, _, UpdateName, DocId|Rest]
  87. }=Req, Db, DDoc) ->
  88. DocParts = [DocId|Rest],
  89. DocId1 = ?l2b(string:join([?b2l(P)|| P <- DocParts], "/")),
  90. Doc = try couch_httpd_db:couch_doc_open(Db, DocId1, nil, [conflicts])
  91. catch
  92. _ -> nil
  93. end,
  94. send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId1);
  95. handle_doc_update_req(#httpd{
  96. path_parts=[_, _, _, _, UpdateName]
  97. }=Req, Db, DDoc) ->
  98. send_doc_update_response(Req, Db, DDoc, UpdateName, nil, null);
  99. handle_doc_update_req(Req, _Db, _DDoc) ->
  100. send_error(Req, 404, <<"update_error">>, <<"Invalid path.">>).
  101. send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) ->
  102. JsonReq = couch_httpd_external:json_req_obj(Req, Db, DocId),
  103. JsonDoc = couch_query_servers:json_doc(Doc),
  104. JsonResp1 = case couch_query_servers:ddoc_prompt(DDoc,
  105. [<<"updates">>, UpdateName], [JsonDoc, JsonReq]) of
  106. [<<"up">>, {NewJsonDoc}, {JsonResp}] ->
  107. Options = case couch_httpd:header_value(Req, "X-Couch-Full-Commit",
  108. "false") of
  109. "true" ->
  110. [full_commit];
  111. _ ->
  112. []
  113. end,
  114. NewDoc = couch_doc:from_json_obj({NewJsonDoc}),
  115. {ok, NewRev} = couch_db:update_doc(Db, NewDoc, Options),
  116. NewRevStr = couch_doc:rev_to_str(NewRev),
  117. {[{<<"code">>, 201}, {<<"headers">>,
  118. {[{<<"X-Couch-Update-NewRev">>, NewRevStr}]}} | JsonResp]};
  119. [<<"up">>, _Other, {JsonResp}] ->
  120. {[{<<"code">>, 200} | JsonResp]}
  121. end,
  122. % todo set location field
  123. couch_httpd_external:send_external_response(Req, JsonResp1).
  124. % view-list request with view and list from same design doc.
  125. handle_view_list_req(#httpd{method='GET',
  126. path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) ->
  127. Keys = couch_httpd:qs_json_value(Req, "keys", nil),
  128. handle_view_list(Req, Db, DDoc, ListName, {DesignName, ViewName}, Keys);
  129. % view-list request with view and list from different design docs.
  130. handle_view_list_req(#httpd{method='GET',
  131. path_parts=[_, _, _, _, ListName, ViewDesignName, ViewName]}=Req, Db, DDoc) ->
  132. Keys = couch_httpd:qs_json_value(Req, "keys", nil),
  133. handle_view_list(Req, Db, DDoc, ListName, {ViewDesignName, ViewName}, Keys);
  134. handle_view_list_req(#httpd{method='GET'}=Req, _Db, _DDoc) ->
  135. send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>);
  136. handle_view_list_req(#httpd{method='POST',
  137. path_parts=[_, _, DesignName, _, ListName, ViewName]}=Req, Db, DDoc) ->
  138. % {Props2} = couch_httpd:json_body(Req),
  139. ReqBody = couch_httpd:body(Req),
  140. {Props2} = ?JSON_DECODE(ReqBody),
  141. Keys = couch_util:get_value(<<"keys">>, Props2, nil),
  142. handle_view_list(Req#httpd{req_body=ReqBody}, Db, DDoc, ListName, {DesignName, ViewName}, Keys);
  143. handle_view_list_req(#httpd{method='POST',
  144. path_parts=[_, _, _, _, ListName, ViewDesignName, ViewName]}=Req, Db, DDoc) ->
  145. % {Props2} = couch_httpd:json_body(Req),
  146. ReqBody = couch_httpd:body(Req),
  147. {Props2} = ?JSON_DECODE(ReqBody),
  148. Keys = couch_util:get_value(<<"keys">>, Props2, nil),
  149. handle_view_list(Req#httpd{req_body=ReqBody}, Db, DDoc, ListName, {ViewDesignName, ViewName}, Keys);
  150. handle_view_list_req(#httpd{method='POST'}=Req, _Db, _DDoc) ->
  151. send_error(Req, 404, <<"list_error">>, <<"Invalid path.">>);
  152. handle_view_list_req(Req, _Db, _DDoc) ->
  153. send_method_not_allowed(Req, "GET,POST,HEAD").
  154. handle_view_list(Req, Db, DDoc, LName, {ViewDesignName, ViewName}, Keys) ->
  155. ViewDesignId = <<"_design/", ViewDesignName/binary>>,
  156. {ViewType, View, Group, QueryArgs} = couch_httpd_view:load_view(Req, Db, {ViewDesignId, ViewName}, Keys),
  157. Etag = list_etag(Req, Db, Group, View, QueryArgs, {couch_httpd:doc_etag(DDoc), Keys}),
  158. couch_httpd:etag_respond(Req, Etag, fun() ->
  159. output_list(ViewType, Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group)
  160. end).
  161. list_etag(#httpd{user_ctx=UserCtx}=Req, Db, Group, View, QueryArgs, More) ->
  162. Accept = couch_httpd:header_value(Req, "Accept"),
  163. couch_httpd_view:view_etag(Db, Group, View, QueryArgs, {More, Accept, UserCtx#user_ctx.roles}).
  164. output_list(map, Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group) ->
  165. output_map_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group);
  166. output_list(reduce, Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group) ->
  167. output_reduce_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group).
  168. % next step:
  169. % use with_ddoc_proc/2 to make this simpler
  170. output_map_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group) ->
  171. #view_query_args{
  172. limit = Limit,
  173. skip = SkipCount
  174. } = QueryArgs,
  175. FoldAccInit = {Limit, SkipCount, undefined, []},
  176. {ok, RowCount} = couch_view:get_row_count(View),
  177. couch_query_servers:with_ddoc_proc(DDoc, fun(QServer) ->
  178. ListFoldHelpers = #view_fold_helper_funs{
  179. reduce_count = fun couch_view:reduce_to_count/1,
  180. start_response = StartListRespFun = make_map_start_resp_fun(QServer, Db, LName),
  181. send_row = make_map_send_row_fun(QServer)
  182. },
  183. CurrentSeq = Group#group.current_seq,
  184. {ok, _, FoldResult} = case Keys of
  185. nil ->
  186. FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, Etag, Db, CurrentSeq, RowCount, ListFoldHelpers),
  187. couch_view:fold(View, FoldlFun, FoldAccInit,
  188. couch_httpd_view:make_key_options(QueryArgs));
  189. Keys ->
  190. lists:foldl(
  191. fun(Key, {ok, _, FoldAcc}) ->
  192. QueryArgs2 = QueryArgs#view_query_args{
  193. start_key = Key,
  194. end_key = Key
  195. },
  196. FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs2, Etag, Db, CurrentSeq, RowCount, ListFoldHelpers),
  197. couch_view:fold(View, FoldlFun, FoldAcc,
  198. couch_httpd_view:make_key_options(QueryArgs2))
  199. end, {ok, nil, FoldAccInit}, Keys)
  200. end,
  201. finish_list(Req, QServer, Etag, FoldResult, StartListRespFun, CurrentSeq, RowCount)
  202. end).
  203. output_reduce_list(Req, Db, DDoc, LName, View, QueryArgs, Etag, Keys, Group) ->
  204. #view_query_args{
  205. limit = Limit,
  206. skip = SkipCount,
  207. group_level = GroupLevel
  208. } = QueryArgs,
  209. CurrentSeq = Group#group.current_seq,
  210. couch_query_servers:with_ddoc_proc(DDoc, fun(QServer) ->
  211. StartListRespFun = make_reduce_start_resp_fun(QServer, Db, LName),
  212. SendListRowFun = make_reduce_send_row_fun(QServer, Db),
  213. {ok, GroupRowsFun, RespFun} = couch_httpd_view:make_reduce_fold_funs(Req,
  214. GroupLevel, QueryArgs, Etag, CurrentSeq,
  215. #reduce_fold_helper_funs{
  216. start_response = StartListRespFun,
  217. send_row = SendListRowFun
  218. }),
  219. FoldAccInit = {Limit, SkipCount, undefined, []},
  220. {ok, FoldResult} = case Keys of
  221. nil ->
  222. couch_view:fold_reduce(View, RespFun, FoldAccInit, [{key_group_fun, GroupRowsFun} |
  223. couch_httpd_view:make_key_options(QueryArgs)]);
  224. Keys ->
  225. lists:foldl(
  226. fun(Key, {ok, FoldAcc}) ->
  227. couch_view:fold_reduce(View, RespFun, FoldAcc,
  228. [{key_group_fun, GroupRowsFun} |
  229. couch_httpd_view:make_key_options(
  230. QueryArgs#view_query_args{start_key=Key, end_key=Key})]
  231. )
  232. end, {ok, FoldAccInit}, Keys)
  233. end,
  234. finish_list(Req, QServer, Etag, FoldResult, StartListRespFun, CurrentSeq, null)
  235. end).
  236. make_map_start_resp_fun(QueryServer, Db, LName) ->
  237. fun(Req, Etag, TotalRows, Offset, _Acc, UpdateSeq) ->
  238. Head = {[{<<"total_rows">>, TotalRows}, {<<"offset">>, Offset}, {<<"update_seq">>, UpdateSeq}]},
  239. start_list_resp(QueryServer, LName, Req, Db, Head, Etag)
  240. end.
  241. make_reduce_start_resp_fun(QueryServer, Db, LName) ->
  242. fun(Req2, Etag, _Acc, UpdateSeq) ->
  243. start_list_resp(QueryServer, LName, Req2, Db, {[{<<"update_seq">>, UpdateSeq}]}, Etag)
  244. end.
  245. start_list_resp(QServer, LName, Req, Db, Head, Etag) ->
  246. JsonReq = couch_httpd_external:json_req_obj(Req, Db),
  247. [<<"start">>,Chunks,JsonResp] = couch_query_servers:ddoc_proc_prompt(QServer,
  248. [<<"lists">>, LName], [Head, JsonReq]),
  249. JsonResp2 = apply_etag(JsonResp, Etag),
  250. #extern_resp_args{
  251. code = Code,
  252. ctype = CType,
  253. headers = ExtHeaders
  254. } = couch_httpd_external:parse_external_response(JsonResp2),
  255. JsonHeaders = couch_httpd_external:default_or_content_type(CType, ExtHeaders),
  256. {ok, Resp} = start_chunked_response(Req, Code, JsonHeaders),
  257. {ok, Resp, ?b2l(?l2b(Chunks))}.
  258. make_map_send_row_fun(QueryServer) ->
  259. fun(Resp, Db, Row, IncludeDocs, Conflicts, RowFront) ->
  260. send_list_row(
  261. Resp, QueryServer, Db, Row, RowFront, IncludeDocs, Conflicts)
  262. end.
  263. make_reduce_send_row_fun(QueryServer, Db) ->
  264. fun(Resp, Row, RowFront) ->
  265. send_list_row(Resp, QueryServer, Db, Row, RowFront, false, false)
  266. end.
  267. send_list_row(Resp, QueryServer, Db, Row, RowFront, IncludeDoc, Conflicts) ->
  268. try
  269. [Go,Chunks] = prompt_list_row(
  270. QueryServer, Db, Row, IncludeDoc, Conflicts),
  271. Chunk = RowFront ++ ?b2l(?l2b(Chunks)),
  272. send_non_empty_chunk(Resp, Chunk),
  273. case Go of
  274. <<"chunks">> ->
  275. {ok, ""};
  276. <<"end">> ->
  277. {stop, stop}
  278. end
  279. catch
  280. throw:Error ->
  281. send_chunked_error(Resp, Error),
  282. throw({already_sent, Resp, Error})
  283. end.
  284. prompt_list_row({Proc, _DDocId}, Db, {{_Key, _DocId}, _} = Kv,
  285. IncludeDoc, Conflicts) ->
  286. JsonRow = couch_httpd_view:view_row_obj(Db, Kv, IncludeDoc, Conflicts),
  287. couch_query_servers:proc_prompt(Proc, [<<"list_row">>, JsonRow]);
  288. prompt_list_row({Proc, _DDocId}, _, {Key, Value}, _IncludeDoc, _Conflicts) ->
  289. JsonRow = {[{key, Key}, {value, Value}]},
  290. couch_query_servers:proc_prompt(Proc, [<<"list_row">>, JsonRow]).
  291. send_non_empty_chunk(Resp, Chunk) ->
  292. case Chunk of
  293. [] -> ok;
  294. _ -> send_chunk(Resp, Chunk)
  295. end.
  296. finish_list(Req, {Proc, _DDocId}, Etag, FoldResult, StartFun, CurrentSeq, TotalRows) ->
  297. FoldResult2 = case FoldResult of
  298. {Limit, SkipCount, Response, RowAcc} ->
  299. {Limit, SkipCount, Response, RowAcc, nil};
  300. Else ->
  301. Else
  302. end,
  303. case FoldResult2 of
  304. {_, _, undefined, _, _} ->
  305. {ok, Resp, BeginBody} =
  306. render_head_for_empty_list(StartFun, Req, Etag, CurrentSeq, TotalRows),
  307. [<<"end">>, Chunks] = couch_query_servers:proc_prompt(Proc, [<<"list_end">>]),
  308. Chunk = BeginBody ++ ?b2l(?l2b(Chunks)),
  309. send_non_empty_chunk(Resp, Chunk);
  310. {_, _, Resp, stop, _} ->
  311. ok;
  312. {_, _, Resp, _, _} ->
  313. [<<"end">>, Chunks] = couch_query_servers:proc_prompt(Proc, [<<"list_end">>]),
  314. send_non_empty_chunk(Resp, ?b2l(?l2b(Chunks)))
  315. end,
  316. last_chunk(Resp).
  317. render_head_for_empty_list(StartListRespFun, Req, Etag, CurrentSeq, null) ->
  318. StartListRespFun(Req, Etag, [], CurrentSeq); % for reduce
  319. render_head_for_empty_list(StartListRespFun, Req, Etag, CurrentSeq, TotalRows) ->
  320. StartListRespFun(Req, Etag, TotalRows, null, [], CurrentSeq).
  321. apply_etag({ExternalResponse}, CurrentEtag) ->
  322. % Here we embark on the delicate task of replacing or creating the
  323. % headers on the JsonResponse object. We need to control the Etag and
  324. % Vary headers. If the external function controls the Etag, we'd have to
  325. % run it to check for a match, which sort of defeats the purpose.
  326. case couch_util:get_value(<<"headers">>, ExternalResponse, nil) of
  327. nil ->
  328. % no JSON headers
  329. % add our Etag and Vary headers to the response
  330. {[{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | ExternalResponse]};
  331. JsonHeaders ->
  332. {[case Field of
  333. {<<"headers">>, JsonHeaders} -> % add our headers
  334. JsonHeadersEtagged = couch_util:json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders),
  335. JsonHeadersVaried = couch_util:json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
  336. {<<"headers">>, JsonHeadersVaried};
  337. _ -> % skip non-header fields
  338. Field
  339. end || Field <- ExternalResponse]}
  340. end.