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
13-module(couch_spatial_http).
14
15-export([handle_spatial_req/3, handle_info_req/3, handle_compact_req/3,
16    handle_cleanup_req/2, parse_qs/1]).
17
18-include("couch_db.hrl").
19-include("couch_spatial.hrl").
20
21-record(acc, {
22    db,
23    req,
24    resp,
25    prepend,
26    etag
27}).
28
29% Either answer a normal spatial query, or keep dispatching if the path part
30% after _spatial starts with an underscore.
31handle_spatial_req(#httpd{
32        path_parts=[_, _, _Dname, _, SpatialName|_]}=Req, Db, DDoc) ->
33    case SpatialName of
34    % the path after _spatial starts with an underscore => dispatch
35    <<$_,_/binary>> ->
36        dispatch_sub_spatial_req(Req, Db, DDoc);
37    _ ->
38        handle_spatial(Req, Db, DDoc)
39    end.
40
41% the dispatching of endpoints below _spatial needs to be done manually
42dispatch_sub_spatial_req(#httpd{
43        path_parts=[_, _, _DName, Spatial, SpatialDisp|_]}=Req,
44        Db, DDoc) ->
45    Conf = couch_config:get("httpd_design_handlers",
46        ?b2l(<<Spatial/binary, "/", SpatialDisp/binary>>)),
47    Fun = couch_httpd:make_arity_3_fun(Conf),
48    apply(Fun, [Req, Db, DDoc]).
49
50
51handle_spatial(#httpd{method='GET'}=Req, Db, DDoc) ->
52    [_, _, _DName, _, ViewName] = Req#httpd.path_parts,
53    couch_stats_collector:increment({httpd, spatial_view_reads}),
54    design_doc_view(Req, Db, DDoc, ViewName);
55handle_spatial(Req, _Db, _DDoc) ->
56    couch_httpd:send_method_not_allowed(Req, "GET,HEAD").
57
58
59handle_info_req(#httpd{method='GET'}=Req, Db, DDoc) ->
60    [_, _, DesignName, _, _] = Req#httpd.path_parts,
61    {ok, GroupInfoList} = couch_spatial:get_info(Db, DDoc),
62    couch_httpd:send_json(Req, 200, {[
63        {name, DesignName},
64        {spatial_index, {GroupInfoList}}
65    ]});
66handle_info_req(Req, _Db, _DDoc) ->
67    couch_httpd:send_method_not_allowed(Req, "GET").
68
69
70handle_compact_req(#httpd{method='POST'}=Req, Db, DDoc) ->
71    ok = couch_db:check_is_admin(Db),
72    couch_httpd:validate_ctype(Req, "application/json"),
73    ok = couch_spatial:compact(Db, DDoc),
74    couch_httpd:send_json(Req, 202, {[{ok, true}]});
75handle_compact_req(Req, _Db, _DDoc) ->
76    couch_httpd:send_method_not_allowed(Req, "POST").
77
78
79handle_cleanup_req(#httpd{method='POST'}=Req, Db) ->
80    % delete unreferenced index files
81    ok = couch_db:check_is_admin(Db),
82    couch_httpd:validate_ctype(Req, "application/json"),
83    ok = couch_spatial:cleanup(Db),
84    couch_httpd:send_json(Req, 202, {[{ok, true}]});
85handle_cleanup_req(Req, _Db) ->
86    couch_httpd:send_method_not_allowed(Req, "POST").
87
88
89
90design_doc_view(Req, Db, DDoc, ViewName) ->
91    Args = parse_qs(Req),
92    design_doc_view(Req, Db, DDoc, ViewName, Args).
93
94design_doc_view(Req, Db, DDoc, ViewName, #spatial_args{count=true}=Args) ->
95    Count = couch_spatial:query_view_count(Db, DDoc, ViewName, Args),
96    couch_httpd:send_json(Req, {[{"count", Count}]});
97design_doc_view(Req, Db, DDoc, ViewName, Args0) ->
98    EtagFun = fun(Sig, Acc0) ->
99        Etag = couch_httpd:make_etag(Sig),
100        case couch_httpd:etag_match(Req, Etag) of
101            true -> throw({etag_match, Etag});
102            false -> {ok, Acc0#acc{etag=Etag}}
103        end
104    end,
105    Args = Args0#spatial_args{preflight_fun=EtagFun},
106    {ok, Resp} = couch_httpd:etag_maybe(Req, fun() ->
107        VAcc = #acc{db=Db, req=Req},
108        couch_spatial:query_view(
109            Db, DDoc, ViewName, Args, fun spatial_cb/2, VAcc)
110    end),
111    case is_record(Resp, acc) of
112        true -> {ok, Resp#acc.resp};
113        _ -> {ok, Resp}
114    end.
115
116
117spatial_cb({meta, Meta}, #acc{resp=undefined}=Acc) ->
118    Headers = [{"ETag", Acc#acc.etag}],
119    {ok, Resp} = couch_httpd:start_json_response(Acc#acc.req, 200, Headers),
120    % Map function starting
121    Parts = case couch_util:get_value(offset, Meta) of
122        undefined -> [];
123        Offset -> [io_lib:format("\"offset\":~p", [Offset])]
124    end ++ case couch_util:get_value(update_seq, Meta) of
125        undefined -> [];
126        UpdateSeq -> [io_lib:format("\"update_seq\":~p", [UpdateSeq])]
127    end ++ ["\"rows\":["],
128    Chunk = lists:flatten("{" ++ string:join(Parts, ",") ++ "\r\n"),
129    couch_httpd:send_chunk(Resp, Chunk),
130    {ok, Acc#acc{resp=Resp, prepend=""}};
131spatial_cb({row, Row}, Acc) ->
132    % Adding another row
133    couch_httpd:send_chunk(
134        Acc#acc.resp, [Acc#acc.prepend, row_to_json(Row)]),
135    {ok, Acc#acc{prepend=",\r\n"}};
136spatial_cb(complete, #acc{resp=undefined}=Acc) ->
137    % Nothing in view
138    {ok, Resp} = couch_httpd:send_json(Acc#acc.req, 200, {[{rows, []}]}),
139    {ok, Acc#acc{resp=Resp}};
140spatial_cb(complete, Acc) ->
141    % Finish view output
142    couch_httpd:send_chunk(Acc#acc.resp, "\r\n]}"),
143    couch_httpd:end_json_response(Acc#acc.resp),
144    {ok, Acc}.
145
146row_to_json(Row) ->
147    ?JSON_ENCODE(couch_spatial_util:row_to_ejson(Row)).
148
149
150parse_qs(Req) ->
151    QueryArgs = lists:foldl(fun({K, V}, Acc) ->
152        parse_qs(K, V, Acc)
153    end, #spatial_args{}, couch_httpd:qs(Req)),
154    lists:foreach(fun
155        ({_, _}) -> ok;
156        (_) ->
157             throw({query_parse_error,
158                 <<"start_range and end_range must be specified">>})
159    end, QueryArgs#spatial_args.range),
160    QueryArgs.
161
162parse_qs(Key, Val, Args) ->
163    case Key of
164    "" ->
165        Args;
166    % bbox is for legacy support. Use start_range and end_range instead.
167    "bbox" ->
168        Error = iolist_to_binary(
169                  [<<"bounding box was invalid, it needs to be bbox=W,S,E,N "
170                     "where each direction is a number. "
171                     "Your bounding box was `">>,
172                   Val, <<"`">>]),
173        try
174            {W, S, E, N} = list_to_tuple(?JSON_DECODE("[" ++ Val ++ "]")),
175            case lists:all(fun(Num) -> is_number(Num) end, [W, S, E, N]) of
176            true ->
177                ok;
178            false ->
179                throw({query_parse_error, Error})
180            end,
181            case W =< E andalso S =< N of
182            true ->
183                Args#spatial_args{range=[{W, E}, {S, N}]};
184            false ->
185                throw({query_parse_error, Error})
186            end
187        catch
188        error:{badmatch, _} ->
189            throw({query_parse_error, Error});
190        throw:{invalid_json, _} ->
191            throw({query_parse_error, Error})
192        end;
193    "stale" when Val == "ok" ->
194        Args#spatial_args{stale=ok};
195    "stale" when Val == "update_after" ->
196        Args#spatial_args{stale=update_after};
197    "stale" ->
198        throw({query_parse_error,
199            <<"stale only available as stale=ok or as stale=update_after">>});
200    "count" when Val == "true" ->
201        Args#spatial_args{count=true};
202    "count" ->
203        throw({query_parse_error, <<"count only available as count=true">>});
204    "limit" ->
205        Args#spatial_args{limit=parse_int(Val)};
206    "skip" ->
207        Args#spatial_args{skip=parse_int(Val)};
208    "start_range" ->
209        Decoded = parse_range(Val),
210        case Args#spatial_args.range of
211        [] ->
212            Args#spatial_args{range=Decoded};
213        % end_range already set the range
214        Range ->
215            Args#spatial_args{range=merge_range(Decoded, Range)}
216        end;
217
218    "end_range" ->
219        Decoded = parse_range(Val),
220        case Args#spatial_args.range of
221        [] ->
222            Args#spatial_args{range=Decoded};
223        % start_range already set the range
224        Range ->
225            Args#spatial_args{range=merge_range(Range, Decoded)}
226        end;
227    _ ->
228        BKey = list_to_binary(Key),
229        BVal = list_to_binary(Val),
230        Args#spatial_args{extra=[{BKey, BVal} | Args#spatial_args.extra]}
231    end.
232
233
234% Verbatim copy from couch_mrview_http
235parse_int(Val) ->
236    case (catch list_to_integer(Val)) of
237    IntVal when is_integer(IntVal) ->
238        IntVal;
239    _ ->
240        Msg = io_lib:format("Invalid value for integer parameter: ~p", [Val]),
241        throw({query_parse_error, ?l2b(Msg)})
242    end.
243
244
245parse_range(Val) ->
246    Decoded = ?JSON_DECODE(Val),
247    case is_list(Decoded) andalso
248        lists:all(fun(Num) -> is_number(Num) orelse Num =:= null end, Decoded) of
249    true ->
250        Decoded;
251    false ->
252        throw({query_parse_error,
253            iolist_to_binary(
254              [<<"range must be an array containing numbers or `null`s. "
255                 "Your range was `">>,
256               Val, "`"])})
257    end.
258
259
260% Merge the start_range and stop_range values into one list
261merge_range(RangeA, RangeB) when length(RangeA) =/= length(RangeB) ->
262    Msg = io_lib:format(
263            "start_range and end_range must have the same number of "
264            "dimensions. Your ranges were ~w and ~w",
265            [RangeA, RangeB]),
266    throw({query_parse_error, ?l2b(Msg)});
267merge_range(RangeA, RangeB) ->
268    lists:zipwith(
269        % `null` is a wildcard which will return all values
270        fun(null, null) ->
271            {nil, nil};
272        % `null` means an open range here
273        (A, null) ->
274            {A, nil};
275        (null, B) ->
276            {nil, B};
277        (A, B) ->
278            {A, B}
279    end, RangeA, RangeB).
280