flm01/server/api/webmachine/src/webmachine_util.erl

298 lines
9.1 KiB
Erlang

%% @author Justin Sheehy <justin@basho.com>
%% @author Andy Gross <andy@basho.com>
%% @copyright 2007-2008 Basho Technologies
%% (guess_mime/1 derived from code copyright 2007 Mochi Media, Inc.)
%%
%% 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 Utilities for parsing, quoting, and negotiation.
-module(webmachine_util).
-export([guess_mime/1]).
-export([convert_request_date/1, compare_ims_dates/2]).
-export([choose_media_type/2]).
-export([choose_charset/2]).
-export([choose_encoding/2]).
-export([unquote_header/1]).
-export([now_diff_milliseconds/2]).
-export([media_type_to_detail/1]).
-export([test/0]).
convert_request_date(Date) ->
try
case httpd_util:convert_request_date(Date) of
ReqDate -> ReqDate
end
catch
error:_ -> bad_date
end.
%% returns true if D1 > D2
compare_ims_dates(D1, D2) ->
GD1 = calendar:datetime_to_gregorian_seconds(D1),
GD2 = calendar:datetime_to_gregorian_seconds(D2),
GD1 > GD2.
%% @spec guess_mime(string()) -> string()
%% @doc Guess the mime type of a file by the extension of its filename.
guess_mime(File) ->
case filename:extension(File) of
".html" ->
"text/html";
".xhtml" ->
"application/xhtml+xml";
".xml" ->
"application/xml";
".css" ->
"text/css";
".js" ->
"application/x-javascript";
".jpg" ->
"image/jpeg";
".jpeg" ->
"image/jpeg";
".gif" ->
"image/gif";
".png" ->
"image/png";
".ico" ->
"image/x-icon";
".swf" ->
"application/x-shockwave-flash";
".zip" ->
"application/zip";
".bz2" ->
"application/x-bzip2";
".gz" ->
"application/x-gzip";
".tar" ->
"application/x-tar";
".tgz" ->
"application/x-gzip";
".htc" ->
"text/x-component";
_ ->
"text/plain"
end.
choose_media_type(Provided,AcceptHead) ->
% Return the Content-Type we will serve for a request.
% If there is no acceptable/available match, return the atom "none".
% AcceptHead is the value of the request's Accept header
% Provided is a list of media types the resource can provide.
% each is either a string e.g. -- "text/html"
% or a string and parameters e.g. -- {"text/html",[{level,1}]}
% (the plain string case with no parameters is much more common)
Requested = accept_header_to_media_types(AcceptHead),
Prov1 = normalize_provided(Provided),
choose_media_type1(Prov1,Requested).
choose_media_type1(_Provided,[]) ->
none;
choose_media_type1(Provided,[H|T]) ->
{_Pri,Type,Params} = H,
case media_match({Type,Params}, Provided) of
[] -> choose_media_type1(Provided,T);
[{CT_T,CT_P}|_] -> format_content_type(CT_T,CT_P)
end.
media_match(_,[]) -> [];
media_match({"*/*",[]},[H|_]) -> [H];
media_match({Type,Params},Provided) ->
[{T1,P1} || {T1,P1} <- Provided,
media_type_match(Type,T1), media_params_match(Params,P1)].
media_type_match(Req,Prov) ->
case Req of
"*" -> % might as well not break for lame (Gomez) clients
true;
"*/*" ->
true;
Prov ->
true;
_ ->
[R1|R2] = string:tokens(Req,"/"),
[P1,_P2] = string:tokens(Prov,"/"),
case R2 of
["*"] ->
case R1 of
P1 -> true;
_ -> false
end;
_ -> false
end
end.
media_params_match(Req,Prov) ->
lists:sort(Req) =:= lists:sort(Prov).
prioritize_media(TyParam) ->
{Type, Params} = TyParam,
prioritize_media(Type,Params,[]).
prioritize_media(Type,Params,Acc) ->
case Params of
[] ->
{1, Type, Acc};
_ ->
[{Tok,Val}|Rest] = Params,
case Tok of
"q" ->
QVal = case Val of
"1" ->
1;
_ -> list_to_float(Val)
end,
{QVal, Type, Rest ++ Acc};
_ ->
prioritize_media(Type,Rest,[{Tok,Val}|Acc])
end
end.
media_type_to_detail(MType) ->
[CType|Params] = string:tokens(MType, ";"),
MParams = [list_to_tuple([string:strip(KV) || KV <- string:tokens(X,"=")])
|| X <- Params],
{CType, MParams}.
accept_header_to_media_types(HeadVal) ->
% given the value of an accept header, produce an ordered list
% based on the q-values. Results are [{Type,Params}] with the
% head of the list being the highest-priority requested type.
try
lists:reverse(lists:keysort(1,
[prioritize_media(media_type_to_detail(MType)) ||
MType <- [string:strip(X) || X <- string:tokens(HeadVal, ",")]]))
catch _:_ -> []
end.
normalize_provided(Provided) ->
[normalize_provided1(X) || X <- Provided].
normalize_provided1(Type) when is_list(Type) -> {Type, []};
normalize_provided1({Type,Params}) -> {Type, Params}.
format_content_type(Type,[]) -> Type;
format_content_type(Type,[H|T]) -> format_content_type(Type ++ "; " ++ H, T).
choose_charset(CSets, AccCharHdr) -> do_choose(CSets, AccCharHdr, "ISO-8859-1").
choose_encoding(Encs, AccEncHdr) -> do_choose(Encs, AccEncHdr, "identity").
do_choose(Choices, Header, Default) ->
Accepted = build_conneg_list(string:tokens(Header, ",")),
DefaultPrio = [P || {P,C} <- Accepted, C =:= Default],
StarPrio = [P || {P,C} <- Accepted, C =:= "*"],
DefaultOkay = case DefaultPrio of
[] ->
case StarPrio of
[0.0] -> no;
_ -> yes
end;
[0.0] -> no;
_ -> yes
end,
AnyOkay = case StarPrio of
[] -> no;
[0.0] -> no;
_ -> yes
end,
do_choose(Default, DefaultOkay, AnyOkay, Choices, Accepted).
do_choose(_Default, _DefaultOkay, _AnyOkay, [], []) ->
none;
do_choose(_Default, _DefaultOkay, _AnyOkay, [], _Accepted) ->
none;
do_choose(Default, DefaultOkay, AnyOkay, Choices, []) ->
case AnyOkay of
yes -> hd(Choices);
no ->
case DefaultOkay of
yes ->
case lists:member(Default, Choices) of
true -> Default;
_ -> none
end;
no -> none
end
end;
do_choose(Default, DefaultOkay, AnyOkay, Choices, [AccPair|AccRest]) ->
{Prio, Acc} = AccPair,
case Prio of
0.0 ->
do_choose(Default, DefaultOkay, AnyOkay,
lists:delete(Acc, Choices), AccRest);
_ ->
LAcc = string:to_lower(Acc),
LChoices = [string:to_lower(X) || X <- Choices],
% doing this a little more work than needed in
% order to be easily insensitive but preserving
case lists:member(LAcc, LChoices) of
true ->
hd([X || X <- Choices,
string:to_lower(X) =:= LAcc]);
false -> do_choose(Default, DefaultOkay, AnyOkay,
Choices, AccRest)
end
end.
build_conneg_list(AccList) ->
build_conneg_list(AccList, []).
build_conneg_list([], Result) -> lists:reverse(lists:sort(Result));
build_conneg_list([Acc|AccRest], Result) ->
XPair = list_to_tuple([string:strip(X) || X <- string:tokens(Acc, ";")]),
Pair = case XPair of
{Choice, "q=" ++ PrioStr} ->
case PrioStr of
"0" -> {0.0, Choice};
"1" -> {1.0, Choice};
_ -> {list_to_float(PrioStr), Choice}
end;
{Choice} ->
{1.0, Choice}
end,
build_conneg_list(AccRest,[Pair|Result]).
% (unquote_header copied from mochiweb_util since they don't export it)
unquote_header("\"" ++ Rest) ->
unquote_header(Rest, []);
unquote_header(S) ->
S.
unquote_header("", Acc) ->
lists:reverse(Acc);
unquote_header("\"", Acc) ->
lists:reverse(Acc);
unquote_header([$\\, C | Rest], Acc) ->
unquote_header(Rest, [C | Acc]);
unquote_header([C | Rest], Acc) ->
unquote_header(Rest, [C | Acc]).
%% @type now() = {MegaSecs, Secs, MicroSecs}
%% This is faster than timer:now_diff() because it does not use bignums.
%% But it returns *milliseconds* (timer:now_diff returns microseconds.)
%% From http://www.erlang.org/ml-archive/erlang-questions/200205/msg00027.html
%% @doc Compute the difference between two now() tuples, in milliseconds.
%% @spec now_diff_milliseconds(now(), now()) -> integer()
now_diff_milliseconds({M,S,U}, {M,S1,U1}) ->
((S-S1) * 1000) + ((U-U1) div 1000);
now_diff_milliseconds({M,S,U}, {M1,S1,U1}) ->
((M-M1)*1000000+(S-S1))*1000 + ((U-U1) div 1000).
test() ->
test_choose_media_type(),
ok.
test_choose_media_type() ->
Provided = "text/html",
ShouldMatch = ["*", "*/*", "text/*", "text/html"],
WantNone = ["foo", "text/xml", "application/*", "foo/bar/baz"],
[ Provided = choose_media_type([Provided], I) || I <- ShouldMatch ],
[ none = choose_media_type([Provided], I) || I <- WantNone ],
ok.