% 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_http). -export([ handle_all_docs_req/2, handle_view_req/3, handle_temp_view_req/2, handle_info_req/3, handle_compact_req/3, handle_cleanup_req/2, parse_qs/2 ]). -include("couch_db.hrl"). -include_lib("couch_mrview/include/couch_mrview.hrl"). -record(vacc, { db, req, resp, prepend, etag }). handle_all_docs_req(#httpd{method='GET'}=Req, Db) -> all_docs_req(Req, Db, undefined); handle_all_docs_req(#httpd{method='POST'}=Req, Db) -> Keys = get_view_keys(couch_httpd:json_body_obj(Req)), all_docs_req(Req, Db, Keys); handle_all_docs_req(Req, _Db) -> couch_httpd:send_method_not_allowed(Req, "GET,POST,HEAD"). handle_view_req(#httpd{method='GET'}=Req, Db, DDoc) -> [_, _, _, _, ViewName] = Req#httpd.path_parts, couch_stats_collector:increment({httpd, view_reads}), design_doc_view(Req, Db, DDoc, ViewName, undefined); handle_view_req(#httpd{method='POST'}=Req, Db, DDoc) -> [_, _, _, _, ViewName] = Req#httpd.path_parts, Keys = get_view_keys(couch_httpd:json_body_obj(Req)), couch_stats_collector:increment({httpd, view_reads}), design_doc_view(Req, Db, DDoc, ViewName, Keys); handle_view_req(Req, _Db, _DDoc) -> couch_httpd:send_method_not_allowed(Req, "GET,POST,HEAD"). handle_temp_view_req(#httpd{method='POST'}=Req, Db) -> couch_httpd:validate_ctype(Req, "application/json"), ok = couch_db:check_is_admin(Db), {Body} = couch_httpd:json_body_obj(Req), DDoc = couch_mrview_util:temp_view_to_ddoc({Body}), Keys = get_view_keys({Body}), couch_stats_collector:increment({httpd, temporary_view_reads}), design_doc_view(Req, Db, DDoc, <<"temp">>, Keys); handle_temp_view_req(Req, _Db) -> couch_httpd:send_method_not_allowed(Req, "POST"). handle_info_req(#httpd{method='GET'}=Req, Db, DDoc) -> [_, _, Name, _] = Req#httpd.path_parts, {ok, Info} = couch_mrview:get_info(Db, DDoc), couch_httpd:send_json(Req, 200, {[ {name, Name}, {view_index, {Info}} ]}); handle_info_req(Req, _Db, _DDoc) -> couch_httpd:send_method_not_allowed(Req, "GET"). handle_compact_req(#httpd{method='POST'}=Req, Db, DDoc) -> ok = couch_db:check_is_admin(Db), couch_httpd:validate_ctype(Req, "application/json"), ok = couch_mrview:compact(Db, DDoc), couch_httpd:send_json(Req, 202, {[{ok, true}]}); handle_compact_req(Req, _Db, _DDoc) -> couch_httpd:send_method_not_allowd(Req, "POST"). handle_cleanup_req(#httpd{method='POST'}=Req, Db) -> ok = couch_db:check_is_admin(Db), couch_httpd:validate_ctype(Req, "application/json"), ok = couch_mrview:cleanup(Db), couch_httpd:send_json(Req, 202, {[{ok, true}]}); handle_cleanup_req(Req, _Db) -> couch_httpd:send_method_not_allowed(Req, "POST"). all_docs_req(Req, Db, Keys) -> Args0 = parse_qs(Req, Keys), ETagFun = fun(Sig, Acc0) -> ETag = couch_httpd:make_etag(Sig), case couch_httpd:etag_match(Req, ETag) of true -> throw({etag_match, ETag}); false -> {ok, Acc0#vacc{etag=ETag}} end end, Args = Args0#mrargs{preflight_fun=ETagFun}, {ok, Resp} = couch_httpd:etag_maybe(Req, fun() -> VAcc0 = #vacc{db=Db, req=Req}, couch_mrview:query_all_docs(Db, Args, fun view_cb/2, VAcc0) end), case is_record(Resp, vacc) of true -> {ok, Resp#vacc.resp}; _ -> {ok, Resp} end. design_doc_view(Req, Db, DDoc, ViewName, Keys) -> Args0 = parse_qs(Req, Keys), ETagFun = fun(Sig, Acc0) -> ETag = couch_httpd:make_etag(Sig), case couch_httpd:etag_match(Req, ETag) of true -> throw({etag_match, ETag}); false -> {ok, Acc0#vacc{etag=ETag}} end end, Args = Args0#mrargs{preflight_fun=ETagFun}, {ok, Resp} = couch_httpd:etag_maybe(Req, fun() -> VAcc0 = #vacc{db=Db, req=Req}, couch_mrview:query_view(Db, DDoc, ViewName, Args, fun view_cb/2, VAcc0) end), case is_record(Resp, vacc) of true -> {ok, Resp#vacc.resp}; _ -> {ok, Resp} end. view_cb({meta, Meta}, #vacc{resp=undefined}=Acc) -> Headers = [{"ETag", Acc#vacc.etag}], {ok, Resp} = couch_httpd:start_json_response(Acc#vacc.req, 200, Headers), % Map function starting Parts = case couch_util:get_value(total, Meta) of undefined -> []; Total -> [io_lib:format("\"total_rows\":~p", [Total])] end ++ case couch_util:get_value(offset, Meta) of undefined -> []; Offset -> [io_lib:format("\"offset\":~p", [Offset])] end ++ case couch_util:get_value(update_seq, Meta) of undefined -> []; UpdateSeq -> [io_lib:format("\"update_seq\":~p", [UpdateSeq])] end ++ ["\"rows\":["], Chunk = lists:flatten("{" ++ string:join(Parts, ",") ++ "\r\n"), couch_httpd:send_chunk(Resp, Chunk), {ok, Acc#vacc{resp=Resp, prepend=""}}; view_cb({row, Row}, #vacc{resp=undefined}=Acc) -> % Reduce function starting Headers = [{"ETag", Acc#vacc.etag}], {ok, Resp} = couch_httpd:start_json_response(Acc#vacc.req, 200, Headers), couch_httpd:send_chunk(Resp, ["{\"rows\":[\r\n", row_to_json(Row)]), {ok, #vacc{resp=Resp, prepend=",\r\n"}}; view_cb({row, Row}, Acc) -> % Adding another row couch_httpd:send_chunk(Acc#vacc.resp, [Acc#vacc.prepend, row_to_json(Row)]), {ok, Acc#vacc{prepend=",\r\n"}}; view_cb(complete, #vacc{resp=undefined}=Acc) -> % Nothing in view {ok, Resp} = couch_httpd:send_json(Acc#vacc.req, 200, {[{rows, []}]}), {ok, Acc#vacc{resp=Resp}}; view_cb(complete, Acc) -> % Finish view output couch_httpd:send_chunk(Acc#vacc.resp, "\r\n]}"), couch_httpd:end_json_response(Acc#vacc.resp), {ok, Acc}. row_to_json(Row) -> Id = couch_util:get_value(id, Row), row_to_json(Id, Row). row_to_json(error, Row) -> % Special case for _all_docs request with KEYS to % match prior behavior. Key = couch_util:get_value(key, Row), Val = couch_util:get_value(value, Row), Obj = {[{key, Key}, {error, Val}]}, ?JSON_ENCODE(Obj); row_to_json(Id0, Row) -> Id = case Id0 of undefined -> []; Id0 -> [{id, Id0}] end, Key = couch_util:get_value(key, Row, null), Val = couch_util:get_value(value, Row), Doc = case couch_util:get_value(doc, Row) of undefined -> []; Doc0 -> [{doc, Doc0}] end, Obj = {Id ++ [{key, Key}, {value, Val}] ++ Doc}, ?JSON_ENCODE(Obj). get_view_keys({Props}) -> case couch_util:get_value(<<"keys">>, Props) of undefined -> ?LOG_DEBUG("POST with no keys member.", []), undefined; Keys when is_list(Keys) -> Keys; _ -> throw({bad_request, "`keys` member must be a array."}) end. parse_qs(Req, Keys) -> Args = #mrargs{keys=Keys}, lists:foldl(fun({K, V}, Acc) -> parse_qs(K, V, Acc) end, Args, couch_httpd:qs(Req)). parse_qs(Key, Val, Args) -> case Key of "" -> Args; "reduce" -> Args#mrargs{reduce=parse_boolean(Val)}; "key" -> JsonKey = ?JSON_DECODE(Val), Args#mrargs{start_key=JsonKey, end_key=JsonKey}; "keys" -> Args#mrargs{keys=?JSON_DECODE(Val)}; "startkey" -> Args#mrargs{start_key=?JSON_DECODE(Val)}; "start_key" -> Args#mrargs{start_key=?JSON_DECODE(Val)}; "startkey_docid" -> Args#mrargs{start_key_docid=list_to_binary(Val)}; "start_key_doc_id" -> Args#mrargs{start_key_docid=list_to_binary(Val)}; "endkey" -> Args#mrargs{end_key=?JSON_DECODE(Val)}; "end_key" -> Args#mrargs{end_key=?JSON_DECODE(Val)}; "endkey_docid" -> Args#mrargs{end_key_docid=list_to_binary(Val)}; "end_key_doc_id" -> Args#mrargs{end_key_docid=list_to_binary(Val)}; "limit" -> Args#mrargs{limit=parse_pos_int(Val)}; "count" -> throw({query_parse_error, <<"QS param `count` is not `limit`">>}); "stale" when Val == "ok" -> Args#mrargs{stale=ok}; "stale" when Val == "update_after" -> Args#mrargs{stale=update_after}; "stale" -> throw({query_parse_error, <<"Invalid value for `stale`.">>}); "descending" -> case parse_boolean(Val) of true -> Args#mrargs{direction=rev}; _ -> Args#mrargs{direction=fwd} end; "skip" -> Args#mrargs{skip=parse_pos_int(Val)}; "group" -> case parse_boolean(Val) of true -> Args#mrargs{group_level=exact}; _ -> Args#mrargs{group_level=0} end; "group_level" -> Args#mrargs{group_level=parse_pos_int(Val)}; "inclusive_end" -> Args#mrargs{inclusive_end=parse_boolean(Val)}; "include_docs" -> Args#mrargs{include_docs=parse_boolean(Val)}; "update_seq" -> Args#mrargs{update_seq=parse_boolean(Val)}; "conflicts" -> Args#mrargs{conflicts=parse_boolean(Val)}; "list" -> Args#mrargs{list=list_to_binary(Val)}; "callback" -> Args#mrargs{callback=list_to_binary(Val)}; _ -> BKey = list_to_binary(Key), BVal = list_to_binary(Val), Args#mrargs{extra=[{BKey, BVal} | Args#mrargs.extra]} end. parse_boolean(Val) -> case string:to_lower(Val) of "true" -> true; "false" -> false; _ -> Msg = io_lib:format("Invalid boolean parameter: ~p", [Val]), throw({query_parse_error, ?l2b(Msg)}) end. parse_int(Val) -> case (catch list_to_integer(Val)) of IntVal when is_integer(IntVal) -> IntVal; _ -> Msg = io_lib:format("Invalid value for integer: ~p", [Val]), throw({query_parse_error, ?l2b(Msg)}) end. parse_pos_int(Val) -> case parse_int(Val) of IntVal when IntVal >= 0 -> IntVal; _ -> Fmt = "Invalid value for positive integer: ~p", Msg = io_lib:format(Fmt, [Val]), throw({query_parse_error, ?l2b(Msg)}) end.