summaryrefslogtreecommitdiff
path: root/deps/chttpd/src/chttpd_view.erl
diff options
context:
space:
mode:
Diffstat (limited to 'deps/chttpd/src/chttpd_view.erl')
-rw-r--r--deps/chttpd/src/chttpd_view.erl393
1 files changed, 393 insertions, 0 deletions
diff --git a/deps/chttpd/src/chttpd_view.erl b/deps/chttpd/src/chttpd_view.erl
new file mode 100644
index 00000000..7e13c356
--- /dev/null
+++ b/deps/chttpd/src/chttpd_view.erl
@@ -0,0 +1,393 @@
+% 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").
+
+-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, Group#group.views, 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, Group#group.views, 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#view.reduce_funs]) of
+ true ->
+ if IsReduce -> reduce; true -> red_map end;
+ false ->
+ case lists:member(ViewName, View#view.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) ->
+ design_doc_view(Req, Db, DDoc, ViewName, nil);
+
+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 BigCouch">>,
+ 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) ->
+ list_to_existing_atom(chttpd:qs_value(Req, "reduce", "true")).
+
+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 = #view_query_args{
+ 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#view_query_args.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_docid, V}];
+parse_json_view_param({<<"endkey_docid">>, V}) ->
+ [{end_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_docid, ?l2b(Value)}];
+parse_view_param("endkey_docid", Value) ->
+ [{end_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#view_query_args.multi_get of
+ true ->
+ Msg = <<"Query parameter `start_key` is "
+ "not compatiible with multi-get">>,
+ throw({query_parse_error, Msg});
+ _ ->
+ Args#view_query_args{start_key=Value}
+ end;
+validate_view_query(start_docid, Value, Args) ->
+ Args#view_query_args{start_docid=Value};
+validate_view_query(end_key, Value, Args) ->
+ case Args#view_query_args.multi_get of
+ true->
+ Msg = <<"Query paramter `end_key` is "
+ "not compatibile with multi-get">>,
+ throw({query_parse_error, Msg});
+ _ ->
+ Args#view_query_args{end_key=Value}
+ end;
+validate_view_query(end_docid, Value, Args) ->
+ Args#view_query_args{end_docid=Value};
+validate_view_query(limit, Value, Args) ->
+ Args#view_query_args{limit=Value};
+validate_view_query(list, Value, Args) ->
+ Args#view_query_args{list=Value};
+validate_view_query(stale, Value, Args) ->
+ Args#view_query_args{stale=Value};
+validate_view_query(descending, true, Args) ->
+ case Args#view_query_args.direction of
+ rev -> Args; % Already reversed
+ fwd ->
+ Args#view_query_args{
+ direction = rev,
+ start_docid =
+ reverse_key_default(Args#view_query_args.start_docid),
+ end_docid =
+ reverse_key_default(Args#view_query_args.end_docid)
+ }
+ end;
+validate_view_query(descending, false, Args) ->
+ Args; % Ignore default condition
+validate_view_query(skip, Value, Args) ->
+ Args#view_query_args{skip=Value};
+validate_view_query(group_level, Value, Args) ->
+ case Args#view_query_args.view_type of
+ reduce ->
+ Args#view_query_args{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#view_query_args{inclusive_end=Value};
+validate_view_query(reduce, false, Args) ->
+ Args;
+validate_view_query(reduce, _, Args) ->
+ case Args#view_query_args.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#view_query_args.view_type of
+ reduce ->
+ Msg = <<"Query paramter `include_docs` "
+ "is invalid for reduce views.">>,
+ throw({query_parse_error, Msg});
+ _ ->
+ Args#view_query_args{include_docs=true}
+ end;
+validate_view_query(include_docs, _Value, Args) ->
+ Args;
+validate_view_query(conflicts, true, Args) ->
+ case Args#view_query_args.view_type of
+ reduce ->
+ Msg = <<"Query parameter `conflicts` "
+ "is invalid for reduce views.">>,
+ throw({query_parse_error, Msg});
+ _ ->
+ Args#view_query_args{conflicts = true}
+ end;
+validate_view_query(conflicts, _Value, Args) ->
+ Args;
+validate_view_query(sorted, false, Args) ->
+ Args#view_query_args{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{sig=Sig,current_seq=CurrentSeq}, _Db, Extra) ->
+ % ?LOG_ERROR("Group ~p",[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.