204 lines
8.2 KiB
Erlang
204 lines
8.2 KiB
Erlang
|
%% @author Robert Ahrens <rahrens@basho.com>
|
||
|
%% @author Justin Sheehy <justin@basho.com>
|
||
|
%% @copyright 2007-2009 Basho Technologies
|
||
|
%%
|
||
|
%% 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.
|
||
|
|
||
|
%% @doc Module for URL-dispatch by pattern matching.
|
||
|
|
||
|
-module(webmachine_dispatcher).
|
||
|
-author('Robert Ahrens <rahrens@basho.com>').
|
||
|
-author('Justin Sheehy <justin@basho.com>').
|
||
|
-author('Bryan Fink <bryan@basho.com>').
|
||
|
|
||
|
-export([dispatch/2, dispatch/3]).
|
||
|
|
||
|
-define(SEPARATOR, $\/).
|
||
|
-define(MATCH_ALL, '*').
|
||
|
|
||
|
%% @spec dispatch(Path::string(), DispatchList::[matchterm()]) ->
|
||
|
%% dispterm() | dispfail()
|
||
|
%% @doc Interface for URL dispatching.
|
||
|
%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
|
||
|
dispatch(PathAsString, DispatchList) ->
|
||
|
dispatch([], PathAsString, DispatchList).
|
||
|
|
||
|
%% @spec dispatch(Host::string(), Path::string(),
|
||
|
%% DispatchList::[matchterm()]) ->
|
||
|
%% dispterm() | dispfail()
|
||
|
%% @doc Interface for URL dispatching.
|
||
|
%% See also http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
|
||
|
dispatch(HostAsString, PathAsString, DispatchList) ->
|
||
|
Path = string:tokens(PathAsString, [?SEPARATOR]),
|
||
|
% URIs that end with a trailing slash are implicitly one token
|
||
|
% "deeper" than we otherwise might think as we are "inside"
|
||
|
% a directory named by the last token.
|
||
|
ExtraDepth = case lists:last(PathAsString) == ?SEPARATOR of
|
||
|
true -> 1;
|
||
|
_ -> 0
|
||
|
end,
|
||
|
{Host, Port} = split_host_port(HostAsString),
|
||
|
try_host_binding(DispatchList, lists:reverse(Host), Port,
|
||
|
Path, ExtraDepth).
|
||
|
|
||
|
split_host_port(HostAsString) ->
|
||
|
case string:tokens(HostAsString, ":") of
|
||
|
[HostPart, PortPart] ->
|
||
|
{split_host(HostPart), list_to_integer(PortPart)};
|
||
|
[HostPart] ->
|
||
|
{split_host(HostPart), 80};
|
||
|
[] ->
|
||
|
%% no host header
|
||
|
{[], 80}
|
||
|
end.
|
||
|
|
||
|
split_host(HostAsString) ->
|
||
|
string:tokens(HostAsString, ".").
|
||
|
|
||
|
%% @type matchterm() = hostmatchterm() | pathmatchterm()
|
||
|
% The dispatch configuration is a list of these terms, and the
|
||
|
% first one whose host and path terms match the input is used.
|
||
|
% Using a pathmatchterm() here is equivalent to using a hostmatchterm()
|
||
|
% of the form {{['*'],'*'}, [pathmatchterm()]}.
|
||
|
|
||
|
%% @type hostmatchterm() = {hostmatch(), [pathmatchterm()]}
|
||
|
% The dispatch configuration contains a list of these terms, and the
|
||
|
% first one whose host and one pathmatchterm match is used.
|
||
|
|
||
|
%% @type hostmatch() = [hostterm()] | {[hostterm()], portterm()}
|
||
|
% A host header (Host, X-Forwarded-For, etc.) will be matched against
|
||
|
% this term. Using a raws [hostterm()] list is equivalent to using
|
||
|
% {[hostterm()], '*'}.
|
||
|
|
||
|
%% @type hostterm() = '*' | string() | atom()
|
||
|
% A list of hostterms is matched against a '.'-separated hostname.
|
||
|
% The '*' hosterm matches all remaining tokens, and is only allowed at
|
||
|
% the head of the list.
|
||
|
% A string hostterm will match a token of exactly the same string.
|
||
|
% Any atom hostterm other than '*' will match any token and will
|
||
|
% create a binding in the result if a complete match occurs.
|
||
|
|
||
|
%% @type portterm() = '*' | integer() | atom()
|
||
|
% A portterm is matched against the integer port after any ':' in
|
||
|
% the hostname, or 80 if no port is found.
|
||
|
% The '*' portterm patches any port
|
||
|
% An integer portterm will match a port of exactly the same integer.
|
||
|
% Any atom portterm other than '*' will match any port and will
|
||
|
% create a binding in the result if a complete match occurs.
|
||
|
|
||
|
%% @type pathmatchterm() = {[pathterm()], matchmod(), matchopts()}.
|
||
|
% The dispatch configuration contains a list of these terms, and the
|
||
|
% first one whose list of pathterms matches the input path is used.
|
||
|
|
||
|
%% @type pathterm() = '*' | string() | atom().
|
||
|
% A list of pathterms is matched against a '/'-separated input path.
|
||
|
% The '*' pathterm matches all remaining tokens.
|
||
|
% A string pathterm will match a token of exactly the same string.
|
||
|
% Any atom pathterm other than '*' will match any token and will
|
||
|
% create a binding in the result if a complete match occurs.
|
||
|
|
||
|
%% @type matchmod() = atom().
|
||
|
% This atom, if present in a successful matchterm, will appear in
|
||
|
% the resulting dispterm. In Webmachine this is used to name the
|
||
|
% resource module that will handle the matching request.
|
||
|
|
||
|
%% @type matchopts() = [term()].
|
||
|
% This term, if present in a successful matchterm, will appear in
|
||
|
% the resulting dispterm. In Webmachine this is used to provide
|
||
|
% arguments to the resource module handling the matching request.
|
||
|
|
||
|
%% @type dispterm() = {matchmod(), matchopts(), pathtokens(),
|
||
|
%% bindings(), approot(), stringpath()}.
|
||
|
|
||
|
%% @type pathtokens() = [pathtoken()].
|
||
|
% This is the list of tokens matched by a trailing '*' pathterm.
|
||
|
|
||
|
%% @type pathtoken() = string().
|
||
|
|
||
|
%% @type bindings() = [{bindingterm(),pathtoken()}].
|
||
|
% This is a proplist of bindings indicated by atom terms in the
|
||
|
% matching spec, bound to the matching tokens in the request path.
|
||
|
|
||
|
%% @type approot() = string().
|
||
|
|
||
|
%% @type stringpath() = string().
|
||
|
% This is the path portion matched by a trailing '*' pathterm.
|
||
|
|
||
|
%% @type dispfail() = {no_dispatch_match, pathtokens()}.
|
||
|
|
||
|
try_host_binding([], Host, Port, Path, _Depth) ->
|
||
|
{no_dispatch_match, {Host, Port}, Path};
|
||
|
try_host_binding([Dispatch|Rest], Host, Port, Path, Depth) ->
|
||
|
{{HostSpec,PortSpec},PathSpec} =
|
||
|
case Dispatch of
|
||
|
{{H,P},S} -> {{H,P},S};
|
||
|
{H,S} -> {{H,?MATCH_ALL},S};
|
||
|
S -> {{[?MATCH_ALL],?MATCH_ALL},[S]}
|
||
|
end,
|
||
|
case bind_port(PortSpec, Port, []) of
|
||
|
{ok, PortBindings} ->
|
||
|
case bind(lists:reverse(HostSpec), Host, PortBindings, 0) of
|
||
|
{ok, HostRemainder, HostBindings, _} ->
|
||
|
case try_path_binding(PathSpec, Path, HostBindings, Depth) of
|
||
|
{Mod, Props, PathRemainder, PathBindings,
|
||
|
AppRoot, StringPath} ->
|
||
|
{Mod, Props, HostRemainder, Port, PathRemainder,
|
||
|
PathBindings, AppRoot, StringPath};
|
||
|
{no_dispatch_match, _} ->
|
||
|
try_host_binding(Rest, Host, Port, Path, Depth)
|
||
|
end;
|
||
|
fail ->
|
||
|
try_host_binding(Rest, Host, Port, Path, Depth)
|
||
|
end;
|
||
|
fail ->
|
||
|
try_host_binding(Rest, Host, Port, Path, Depth)
|
||
|
end.
|
||
|
|
||
|
bind_port(Port, Port, Bindings) -> {ok, Bindings};
|
||
|
bind_port(?MATCH_ALL, _Port, Bindings) -> {ok, Bindings};
|
||
|
bind_port(PortAtom, Port, Bindings) when is_atom(PortAtom) ->
|
||
|
{ok, [{PortAtom, Port}|Bindings]};
|
||
|
bind_port(_, _, _) -> fail.
|
||
|
|
||
|
try_path_binding([], PathTokens, _, _) ->
|
||
|
{no_dispatch_match, PathTokens};
|
||
|
try_path_binding([{PathSchema, Mod, Props}|Rest], PathTokens,
|
||
|
Bindings, ExtraDepth) ->
|
||
|
case bind(PathSchema, PathTokens, Bindings, 0) of
|
||
|
{ok, Remainder, NewBindings, Depth} ->
|
||
|
{Mod, Props, Remainder, NewBindings,
|
||
|
calculate_app_root(Depth + ExtraDepth), reconstitute(Remainder)};
|
||
|
fail ->
|
||
|
try_path_binding(Rest, PathTokens, Bindings, ExtraDepth)
|
||
|
end.
|
||
|
|
||
|
bind([], [], Bindings, Depth) ->
|
||
|
{ok, [], Bindings, Depth};
|
||
|
bind([?MATCH_ALL], Rest, Bindings, Depth) when is_list(Rest) ->
|
||
|
{ok, Rest, Bindings, Depth + length(Rest)};
|
||
|
bind(_, [], _, _) ->
|
||
|
fail;
|
||
|
bind([Token|RestToken],[Match|RestMatch],Bindings,Depth) when is_atom(Token) ->
|
||
|
bind(RestToken, RestMatch, [{Token, Match}|Bindings], Depth + 1);
|
||
|
bind([Token|RestToken], [Token|RestMatch], Bindings, Depth) ->
|
||
|
bind(RestToken, RestMatch, Bindings, Depth + 1);
|
||
|
bind(_, _, _, _) ->
|
||
|
fail.
|
||
|
|
||
|
reconstitute([]) -> "";
|
||
|
reconstitute(UnmatchedTokens) -> string:join(UnmatchedTokens, [?SEPARATOR]).
|
||
|
|
||
|
calculate_app_root(1) -> ".";
|
||
|
calculate_app_root(N) when N > 1 ->
|
||
|
string:join(lists:duplicate(N, ".."), [?SEPARATOR]).
|