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_httpd_spatial_merger).
14
15-export([handle_req/1]).
16
17-include("couch_db.hrl").
18-include_lib("couch_index_merger/include/couch_index_merger.hrl").
19-include("../lhttpc/lhttpc.hrl").
20
21-import(couch_util, [
22    get_value/2,
23    get_value/3,
24    to_binary/1
25]).
26-import(couch_httpd, [
27    qs_json_value/3
28]).
29
30
31handle_req(#httpd{method = 'GET'} = Req) ->
32    Indexes = validate_spatial_param(qs_json_value(Req, "spatial", nil)),
33    DDocRevision = couch_index_merger:validate_revision_param(
34        qs_json_value(Req, <<"ddoc_revision">>, nil)),
35    MergeParams0 = #index_merge{
36        indexes = Indexes,
37        ddoc_revision = DDocRevision
38    },
39    MergeParams1 = couch_httpd_view_merger:apply_http_config(
40        Req, [], MergeParams0),
41    couch_index_merger:query_index(couch_spatial_merger, MergeParams1, Req);
42
43handle_req(#httpd{method = 'POST'} = Req) ->
44    couch_httpd:validate_ctype(Req, "application/json"),
45    {Props} = couch_httpd:json_body_obj(Req),
46    Indexes = validate_spatial_param(get_value(<<"spatial">>, Props)),
47    DDocRevision = couch_index_merger:validate_revision_param(
48        get_value(<<"ddoc_revision">>, Props, nil)),
49    MergeParams0 = #index_merge{
50        indexes = Indexes,
51        ddoc_revision = DDocRevision
52    },
53    MergeParams1 = couch_httpd_view_merger:apply_http_config(
54        Req, Props, MergeParams0),
55    couch_index_merger:query_index(couch_spatial_merger, MergeParams1, Req);
56
57handle_req(Req) ->
58    couch_httpd:send_method_not_allowed(Req, "GET,POST").
59
60%% Valid `spatial` example:
61%%
62%% {
63%%   "spatial": {
64%%     "localdb1": ["ddocname/spatialname", ...],
65%%     "http://server2/dbname": ["ddoc/spatial"],
66%%     "http://server2/_spatial_merge": {
67%%       "spatial": {
68%%         "localdb3": "spatialname", // local to server2
69%%         "localdb4": "spatialname"  // local to server2
70%%       }
71%%     }
72%%   }
73%% }
74
75validate_spatial_param({[_ | _] = Indexes}) ->
76    lists:flatten(lists:map(
77        fun({DbName, SpatialName}) when is_binary(SpatialName) ->
78            {DDocDbName, DDocId, Vn} = parse_spatial_name(SpatialName),
79            #simple_index_spec{
80                database = DbName, ddoc_id = DDocId, index_name = Vn,
81                ddoc_database = DDocDbName
82            };
83        ({DbName, SpatialNames}) when is_list(SpatialNames) ->
84            lists:map(
85                fun(SpatialName) ->
86                    {DDocDbName, DDocId, Vn} = parse_spatial_name(SpatialName),
87                    #simple_index_spec{
88                        database = DbName, ddoc_id = DDocId, index_name = Vn,
89                        ddoc_database = DDocDbName
90                    }
91                end, SpatialNames);
92        ({MergeUrl, {[_ | _] = Props} = EJson}) ->
93            case (catch lhttpc_lib:parse_url(?b2l(MergeUrl))) of
94            #lhttpc_url{} ->
95                ok;
96            _ ->
97                throw({bad_request, "Invalid spatial merge definition object."})
98            end,
99            case get_value(<<"ddoc_revision">>, Props) of
100            undefined ->
101                ok;
102            _ ->
103                Msg = "Nested 'ddoc_revision' specifications are not allowed.",
104                throw({bad_request, Msg})
105            end,
106            case get_value(<<"spatial">>, Props) of
107            {[_ | _]} = SubSpatial ->
108                SubSpatialSpecs = validate_spatial_param(SubSpatial),
109                case lists:any(
110                    fun(#simple_index_spec{}) -> true; (_) -> false end,
111                    SubSpatialSpecs) of
112                true ->
113                    ok;
114                false ->
115                    SubMergeError = io_lib:format("Could not find a"
116                        " non-composed spatial spec in the spatial merge"
117                        " targeted at `~s`",
118                        [couch_index_merger:rem_passwd(MergeUrl)]),
119                    throw({bad_request, SubMergeError})
120                end,
121                #merged_index_spec{url = MergeUrl, ejson_spec = EJson};
122            _ ->
123                SubMergeError = io_lib:format("Invalid spatial merge"
124                    " definition for sub-merge done at `~s`.",
125                    [couch_index_merger:rem_passwd(MergeUrl)]),
126                throw({bad_request, SubMergeError})
127            end;
128        (_) ->
129            throw({bad_request, "Invalid spatial merge definition object."})
130        end, Indexes));
131
132validate_spatial_param(_) ->
133    throw({bad_request, <<"`spatial` parameter must be an object with at ",
134                          "least 1 property.">>}).
135
136parse_spatial_name(Name) ->
137    case string:tokens(couch_util:trim(?b2l(Name)), "/") of
138    [DDocName, ViewName0] ->
139        {nil, <<"_design/", (?l2b(DDocName))/binary>>, ?l2b(ViewName0)};
140    ["_design", DDocName, ViewName0] ->
141        {nil, <<"_design/", (?l2b(DDocName))/binary>>, ?l2b(ViewName0)};
142    [DDocDbName1, DDocName, ViewName0] ->
143        DDocDbName = ?l2b(couch_httpd:unquote(DDocDbName1)),
144        {DDocDbName, <<"_design/", (?l2b(DDocName))/binary>>, ?l2b(ViewName0)};
145    [DDocDbName1, "_design", DDocName, ViewName0] ->
146        DDocDbName = ?l2b(couch_httpd:unquote(DDocDbName1)),
147        {DDocDbName, <<"_design/", (?l2b(DDocName))/binary>>, ?l2b(ViewName0)};
148    _ ->
149        throw({bad_request, "A `spatial` property must have the shape"
150            " `ddoc_name/spatial_name`."})
151    end.
152