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" ->
167        {W, S, E, N} = list_to_tuple(?JSON_DECODE("[" ++ Val ++ "]")),
168        Args#spatial_args{bbox=[{W, E}, {S, N}]};
169    "stale" when Val == "ok" ->
170        Args#spatial_args{stale=ok};
171    "stale" when Val == "update_after" ->
172        Args#spatial_args{stale=update_after};
173    "stale" ->
174        throw({query_parse_error,
175            <<"stale only available as stale=ok or as stale=update_after">>});
176    "count" when Val == "true" ->
177        Args#spatial_args{count=true};
178    "count" ->
179        throw({query_parse_error, <<"count only available as count=true">>});
180    "plane_bounds" ->
181        {W, S, E, N} = list_to_tuple(?JSON_DECODE("[" ++ Val ++ "]")),
182        Args#spatial_args{bounds=[{W, E}, {S, N}]};
183    "limit" ->
184        Args#spatial_args{limit=parse_int(Val)};
185    "skip" ->
186        Args#spatial_args{skip=parse_int(Val)};
187    "start_range" ->
188        case Args#spatial_args.range of
189        nil ->
190            Args#spatial_args{range=?JSON_DECODE(Val)};
191        % end_range already set the range
192        Range ->
193            Args#spatial_args{range=merge_range(?JSON_DECODE(Val), Range)}
194        end;
195    "end_range" ->
196        case Args#spatial_args.range of
197        nil ->
198            Args#spatial_args{range=?JSON_DECODE(Val)};
199        % start_range already set the range
200        Range ->
201            Args#spatial_args{range=merge_range(Range, ?JSON_DECODE(Val))}
202        end;
203    _ ->
204        BKey = list_to_binary(Key),
205        BVal = list_to_binary(Val),
206        Args#spatial_args{extra=[{BKey, BVal} | Args#spatial_args.extra]}
207    end.
208
209
210% Verbatim copy from couch_mrview_http
211parse_int(Val) ->
212    case (catch list_to_integer(Val)) of
213    IntVal when is_integer(IntVal) ->
214        IntVal;
215    _ ->
216        Msg = io_lib:format("Invalid value for integer parameter: ~p", [Val]),
217        throw({query_parse_error, ?l2b(Msg)})
218    end.
219
220
221% Merge the start_range and stop_range values into one list
222merge_range(RangeA, RangeB) ->
223    lists:zipwith(
224        % `null` is a wildcard which will return all values
225        fun(null, null) ->
226            {nil, nil};
227        % `null` means an open range here
228        (A, null) ->
229            {A, nil};
230        (null, B) ->
231            {nil, B};
232        (A, B) ->
233            {A, B}
234    end, RangeA, RangeB).
235