| % 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_view). |
| -include_lib("couch/include/couch_db.hrl"). |
| -include_lib("couch_mrview/include/couch_mrview.hrl"). |
| |
| -export([handle_view_req/3, handle_temp_view_req/2, get_reduce_type/1, |
| parse_view_params/3, view_group_etag/2, view_group_etag/3, |
| parse_bool_param/1, extract_view_type/3]). |
| |
| |
| multi_query_view(Req, Db, DDoc, ViewName, Queries) -> |
| Group = couch_view_group:design_doc_to_view_group(DDoc), |
| IsReduce = get_reduce_type(Req), |
| ViewType = extract_view_type(ViewName, couch_view_group:get_views(Group), |
| IsReduce), |
| % TODO proper calculation of etag |
| % Etag = view_group_etag(ViewGroup, Db, Queries), |
| Etag = couch_uuids:new(), |
| DefaultParams = lists:flatmap(fun({K,V}) -> parse_view_param(K,V) end, |
| chttpd:qs(Req)), |
| [couch_stats_collector:increment({httpd, view_reads}) || _I <- Queries], |
| chttpd:etag_respond(Req, Etag, fun() -> |
| FirstChunk = "{\"results\":[", |
| {ok, Resp} = chttpd:start_delayed_json_response(Req, 200, [{"Etag",Etag}], FirstChunk), |
| {_, Resp1} = lists:foldl(fun({QueryProps}, {Chunk, RespAcc}) -> |
| if Chunk =/= nil -> chttpd:send_delayed_chunk(Resp, Chunk); true -> ok end, |
| ThisQuery = lists:flatmap(fun parse_json_view_param/1, QueryProps), |
| FullParams = lists:ukeymerge(1, ThisQuery, DefaultParams), |
| {ok, RespAcc1} = fabric:query_view( |
| Db, |
| DDoc, |
| ViewName, |
| fun view_callback/2, |
| {nil, RespAcc}, |
| parse_view_params(FullParams, nil, ViewType) |
| ), |
| {",\n", RespAcc1} |
| end, {nil,Resp}, Queries), |
| chttpd:send_delayed_chunk(Resp1, "]}"), |
| chttpd:end_delayed_json_response(Resp1) |
| end). |
| |
| design_doc_view(Req, Db, DDoc, ViewName, Keys) -> |
| Group = couch_view_group:design_doc_to_view_group(DDoc), |
| IsReduce = get_reduce_type(Req), |
| ViewType = extract_view_type(ViewName, couch_view_group:get_views(Group), |
| IsReduce), |
| QueryArgs = parse_view_params(Req, Keys, ViewType), |
| % TODO proper calculation of etag |
| % Etag = view_group_etag(ViewGroup, Db, Keys), |
| Etag = couch_uuids:new(), |
| couch_stats_collector:increment({httpd, view_reads}), |
| chttpd:etag_respond(Req, Etag, fun() -> |
| {ok, Resp} = chttpd:start_delayed_json_response(Req, 200, [{"Etag",Etag}]), |
| CB = fun view_callback/2, |
| {ok, Resp1} = fabric:query_view(Db, DDoc, ViewName, CB, {nil, Resp}, QueryArgs), |
| chttpd:end_delayed_json_response(Resp1) |
| end). |
| |
| view_callback({total_and_offset, Total, Offset}, {nil, Resp}) -> |
| Chunk = "{\"total_rows\":~p,\"offset\":~p,\"rows\":[\r\n", |
| {ok, Resp1} = chttpd:send_delayed_chunk(Resp, io_lib:format(Chunk, [Total, Offset])), |
| {ok, {"", Resp1}}; |
| view_callback({total_and_offset, _, _}, Acc) -> |
| % a sorted=false view where the message came in late. Ignore. |
| {ok, Acc}; |
| view_callback({row, Row}, {nil, Resp}) -> |
| % first row of a reduce view, or a sorted=false view |
| {ok, Resp1} = chttpd:send_delayed_chunk(Resp, ["{\"rows\":[\r\n", ?JSON_ENCODE(Row)]), |
| {ok, {",\r\n", Resp1}}; |
| view_callback({row, Row}, {Prepend, Resp}) -> |
| {ok, Resp1} = chttpd:send_delayed_chunk(Resp, [Prepend, ?JSON_ENCODE(Row)]), |
| {ok, {",\r\n", Resp1}}; |
| view_callback(complete, {nil, Resp}) -> |
| chttpd:send_delayed_chunk(Resp, "{\"rows\":[]}"); |
| view_callback(complete, {_, Resp}) -> |
| chttpd:send_delayed_chunk(Resp, "\r\n]}"); |
| view_callback({error, Reason}, {_, Resp}) -> |
| chttpd:send_delayed_error(Resp, Reason). |
| |
| extract_view_type(_ViewName, [], _IsReduce) -> |
| throw({not_found, missing_named_view}); |
| extract_view_type(ViewName, [View|Rest], IsReduce) -> |
| case lists:member(ViewName, [Name || {Name, _} <- View#mrview.reduce_funs]) of |
| true -> |
| if IsReduce -> reduce; true -> red_map end; |
| false -> |
| case lists:member(ViewName, View#mrview.map_names) of |
| true -> map; |
| false -> extract_view_type(ViewName, Rest, IsReduce) |
| end |
| end. |
| |
| handle_view_req(#httpd{method='GET', |
| path_parts=[_, _, _, _, ViewName]}=Req, Db, DDoc) -> |
| Keys = chttpd:qs_json_value(Req, "keys", nil), |
| design_doc_view(Req, Db, DDoc, ViewName, Keys); |
| |
| handle_view_req(#httpd{method='POST', |
| path_parts=[_, _, _, _, ViewName]}=Req, Db, DDoc) -> |
| {Fields} = chttpd:json_body_obj(Req), |
| Queries = couch_util:get_value(<<"queries">>, Fields), |
| Keys = couch_util:get_value(<<"keys">>, Fields), |
| case {Queries, Keys} of |
| {Queries, undefined} when is_list(Queries) -> |
| multi_query_view(Req, Db, DDoc, ViewName, Queries); |
| {undefined, Keys} when is_list(Keys) -> |
| design_doc_view(Req, Db, DDoc, ViewName, Keys); |
| {undefined, undefined} -> |
| throw({bad_request, "POST body must contain `keys` or `queries` field"}); |
| {undefined, _} -> |
| throw({bad_request, "`keys` body member must be an array"}); |
| {_, undefined} -> |
| throw({bad_request, "`queries` body member must be an array"}); |
| {_, _} -> |
| throw({bad_request, "`keys` and `queries` are mutually exclusive"}) |
| end; |
| |
| handle_view_req(Req, _Db, _DDoc) -> |
| chttpd:send_method_not_allowed(Req, "GET,POST,HEAD"). |
| |
| handle_temp_view_req(Req, _Db) -> |
| Msg = <<"Temporary views are not supported in CouchDB">>, |
| chttpd:send_error(Req, 403, forbidden, Msg). |
| |
| reverse_key_default(?MIN_STR) -> ?MAX_STR; |
| reverse_key_default(?MAX_STR) -> ?MIN_STR; |
| reverse_key_default(Key) -> Key. |
| |
| get_reduce_type(Req) -> |
| case chttpd:qs_value(Req, "reduce", "true") of |
| "true" -> |
| true; |
| "false" -> |
| false; |
| _Error -> |
| throw({bad_request, "`reduce` qs param must be `true` or `false`"}) |
| end. |
| |
| parse_view_params(Req, Keys, ViewType) when not is_list(Req) -> |
| QueryParams = lists:flatmap(fun({K,V}) -> parse_view_param(K,V) end, |
| chttpd:qs(Req)), |
| parse_view_params(QueryParams, Keys, ViewType); |
| parse_view_params(QueryParams, Keys, ViewType) -> |
| IsMultiGet = (Keys =/= nil), |
| Args = #mrargs{ |
| view_type=ViewType, |
| multi_get=IsMultiGet, |
| keys=Keys |
| }, |
| QueryArgs = lists:foldl(fun({K, V}, Args2) -> |
| validate_view_query(K, V, Args2) |
| end, Args, QueryParams), |
| |
| GroupLevel = QueryArgs#mrargs.group_level, |
| case {ViewType, GroupLevel, IsMultiGet} of |
| {reduce, exact, true} -> |
| QueryArgs; |
| {reduce, _, false} -> |
| QueryArgs; |
| {reduce, _, _} -> |
| Msg = <<"Multi-key fetchs for reduce " |
| "view must include `group=true`">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| QueryArgs |
| end, |
| QueryArgs. |
| |
| parse_json_view_param({<<"key">>, V}) -> |
| [{start_key, V}, {end_key, V}]; |
| parse_json_view_param({<<"startkey_docid">>, V}) -> |
| [{start_key_docid, V}]; |
| parse_json_view_param({<<"endkey_docid">>, V}) -> |
| [{end_key_docid, V}]; |
| parse_json_view_param({<<"startkey">>, V}) -> |
| [{start_key, V}]; |
| parse_json_view_param({<<"endkey">>, V}) -> |
| [{end_key, V}]; |
| parse_json_view_param({<<"limit">>, V}) when is_integer(V), V > 0 -> |
| [{limit, V}]; |
| parse_json_view_param({<<"stale">>, <<"ok">>}) -> |
| [{stale, ok}]; |
| parse_json_view_param({<<"stale">>, <<"update_after">>}) -> |
| [{stale, update_after}]; |
| parse_json_view_param({<<"descending">>, V}) when is_boolean(V) -> |
| [{descending, V}]; |
| parse_json_view_param({<<"skip">>, V}) when is_integer(V) -> |
| [{skip, V}]; |
| parse_json_view_param({<<"group">>, true}) -> |
| [{group_level, exact}]; |
| parse_json_view_param({<<"group">>, false}) -> |
| [{group_level, 0}]; |
| parse_json_view_param({<<"group_level">>, V}) when is_integer(V), V > 0 -> |
| [{group_level, V}]; |
| parse_json_view_param({<<"inclusive_end">>, V}) when is_boolean(V) -> |
| [{inclusive_end, V}]; |
| parse_json_view_param({<<"reduce">>, V}) when is_boolean(V) -> |
| [{reduce, V}]; |
| parse_json_view_param({<<"include_docs">>, V}) when is_boolean(V) -> |
| [{include_docs, V}]; |
| parse_json_view_param({<<"conflicts">>, V}) when is_boolean(V) -> |
| [{conflicts, V}]; |
| parse_json_view_param({<<"list">>, V}) -> |
| [{list, couch_util:to_binary(V)}]; |
| parse_json_view_param({<<"sorted">>, V}) when is_boolean(V) -> |
| [{sorted, V}]; |
| parse_json_view_param({K, V}) -> |
| [{extra, {K, V}}]. |
| |
| parse_view_param("", _) -> |
| []; |
| parse_view_param("key", Value) -> |
| JsonKey = ?JSON_DECODE(Value), |
| [{start_key, JsonKey}, {end_key, JsonKey}]; |
| parse_view_param("startkey_docid", Value) -> |
| [{start_key_docid, ?l2b(Value)}]; |
| parse_view_param("endkey_docid", Value) -> |
| [{end_key_docid, ?l2b(Value)}]; |
| parse_view_param("startkey", Value) -> |
| [{start_key, ?JSON_DECODE(Value)}]; |
| parse_view_param("endkey", Value) -> |
| [{end_key, ?JSON_DECODE(Value)}]; |
| parse_view_param("limit", Value) -> |
| [{limit, parse_positive_int_param(Value)}]; |
| parse_view_param("count", _Value) -> |
| throw({query_parse_error, <<"Query parameter 'count' is now 'limit'.">>}); |
| parse_view_param("stale", "ok") -> |
| [{stale, ok}]; |
| parse_view_param("stale", "update_after") -> |
| [{stale, update_after}]; |
| parse_view_param("stale", _Value) -> |
| throw({query_parse_error, |
| <<"stale only available as stale=ok or as stale=update_after">>}); |
| parse_view_param("update", _Value) -> |
| throw({query_parse_error, <<"update=false is now stale=ok">>}); |
| parse_view_param("descending", Value) -> |
| [{descending, parse_bool_param(Value)}]; |
| parse_view_param("skip", Value) -> |
| [{skip, parse_int_param(Value)}]; |
| parse_view_param("group", Value) -> |
| case parse_bool_param(Value) of |
| true -> [{group_level, exact}]; |
| false -> [{group_level, 0}] |
| end; |
| parse_view_param("group_level", Value) -> |
| [{group_level, parse_positive_int_param(Value)}]; |
| parse_view_param("inclusive_end", Value) -> |
| [{inclusive_end, parse_bool_param(Value)}]; |
| parse_view_param("reduce", Value) -> |
| [{reduce, parse_bool_param(Value)}]; |
| parse_view_param("include_docs", Value) -> |
| [{include_docs, parse_bool_param(Value)}]; |
| parse_view_param("conflicts", Value) -> |
| [{conflicts, parse_bool_param(Value)}]; |
| parse_view_param("list", Value) -> |
| [{list, ?l2b(Value)}]; |
| parse_view_param("callback", _) -> |
| []; % Verified in the JSON response functions |
| parse_view_param("sorted", Value) -> |
| [{sorted, parse_bool_param(Value)}]; |
| parse_view_param(Key, Value) -> |
| [{extra, {Key, Value}}]. |
| |
| validate_view_query(start_key, Value, Args) -> |
| case Args#mrargs.multi_get of |
| true -> |
| Msg = <<"Query parameter `start_key` is " |
| "not compatible with multi-get">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#mrargs{start_key=Value} |
| end; |
| validate_view_query(start_key_docid, Value, Args) -> |
| Args#mrargs{start_key_docid=Value}; |
| validate_view_query(end_key, Value, Args) -> |
| case Args#mrargs.multi_get of |
| true-> |
| Msg = <<"Query parameter `end_key` is " |
| "not compatible with multi-get">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#mrargs{end_key=Value} |
| end; |
| validate_view_query(end_key_docid, Value, Args) -> |
| Args#mrargs{end_key_docid=Value}; |
| validate_view_query(limit, Value, Args) -> |
| Args#mrargs{limit=Value}; |
| validate_view_query(list, Value, Args) -> |
| Args#mrargs{list=Value}; |
| validate_view_query(stale, Value, Args) -> |
| Args#mrargs{stale=Value}; |
| validate_view_query(descending, true, Args) -> |
| case Args#mrargs.direction of |
| rev -> Args; % Already reversed |
| fwd -> |
| Args#mrargs{ |
| direction = rev, |
| start_key_docid = |
| reverse_key_default(Args#mrargs.start_key_docid), |
| end_key_docid = |
| reverse_key_default(Args#mrargs.end_key_docid) |
| } |
| end; |
| validate_view_query(descending, false, Args) -> |
| Args; % Ignore default condition |
| validate_view_query(skip, Value, Args) -> |
| Args#mrargs{skip=Value}; |
| validate_view_query(group_level, Value, Args) -> |
| case Args#mrargs.view_type of |
| reduce -> |
| Args#mrargs{group_level=Value}; |
| _ -> |
| Msg = <<"Invalid URL parameter 'group' or " |
| " 'group_level' for non-reduce view.">>, |
| throw({query_parse_error, Msg}) |
| end; |
| validate_view_query(inclusive_end, Value, Args) -> |
| Args#mrargs{inclusive_end=Value}; |
| validate_view_query(reduce, false, Args) -> |
| Args; |
| validate_view_query(reduce, _, Args) -> |
| case Args#mrargs.view_type of |
| map -> |
| Msg = <<"Invalid URL parameter `reduce` for map view.">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args |
| end; |
| validate_view_query(include_docs, true, Args) -> |
| case Args#mrargs.view_type of |
| reduce -> |
| Msg = <<"Query parameter `include_docs` " |
| "is invalid for reduce views.">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#mrargs{include_docs=true} |
| end; |
| validate_view_query(include_docs, _Value, Args) -> |
| Args; |
| validate_view_query(conflicts, true, Args) -> |
| case Args#mrargs.view_type of |
| reduce -> |
| Msg = <<"Query parameter `conflicts` " |
| "is invalid for reduce views.">>, |
| throw({query_parse_error, Msg}); |
| _ -> |
| Args#mrargs{extra = [conflicts|Args#mrargs.extra]} |
| end; |
| validate_view_query(conflicts, _Value, Args) -> |
| Args; |
| validate_view_query(sorted, false, Args) -> |
| Args#mrargs{sorted=false}; |
| validate_view_query(sorted, _Value, Args) -> |
| Args; |
| validate_view_query(extra, _Value, Args) -> |
| Args. |
| |
| view_group_etag(Group, Db) -> |
| view_group_etag(Group, Db, nil). |
| |
| view_group_etag(Group, _Db, Extra) -> |
| Sig = couch_view_group:get_signature(Group), |
| CurrentSeq = couch_view_group:get_current_seq(Group), |
| % This is not as granular as it could be. |
| % If there are updates to the db that do not effect the view index, |
| % they will change the Etag. For more granular Etags we'd need to keep |
| % track of the last Db seq that caused an index change. |
| chttpd:make_etag({Sig, CurrentSeq, Extra}). |
| |
| parse_bool_param("true") -> true; |
| parse_bool_param("false") -> false; |
| parse_bool_param(Val) -> |
| Msg = io_lib:format("Invalid value for boolean paramter: ~p", [Val]), |
| throw({query_parse_error, ?l2b(Msg)}). |
| |
| parse_int_param(Val) -> |
| case (catch list_to_integer(Val)) of |
| IntVal when is_integer(IntVal) -> |
| IntVal; |
| _ -> |
| Msg = io_lib:format("Invalid value for integer parameter: ~p", [Val]), |
| throw({query_parse_error, ?l2b(Msg)}) |
| end. |
| |
| parse_positive_int_param(Val) -> |
| case parse_int_param(Val) of |
| IntVal when IntVal >= 0 -> |
| IntVal; |
| _ -> |
| Fmt = "Invalid value for positive integer parameter: ~p", |
| Msg = io_lib:format(Fmt, [Val]), |
| throw({query_parse_error, ?l2b(Msg)}) |
| end. |