Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 1EE9710BBA for ; Fri, 17 Jan 2014 22:54:07 +0000 (UTC) Received: (qmail 42198 invoked by uid 500); 17 Jan 2014 22:52:49 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 41985 invoked by uid 500); 17 Jan 2014 22:52:45 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 41698 invoked by uid 99); 17 Jan 2014 22:52:40 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 17 Jan 2014 22:52:40 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id 2796337B00; Fri, 17 Jan 2014 22:52:39 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: davisp@apache.org To: commits@couchdb.apache.org Date: Fri, 17 Jan 2014 22:53:06 -0000 Message-Id: <51d4fdc9c46e439da9633de174c62532@git.apache.org> In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [29/50] git commit: Added mochiweb_request:accept_content_type/1 function. This function allows a caller to know if a request accepts a given media type. Examples: Added mochiweb_request:accept_content_type/1 function. This function allows a caller to know if a request accepts a given media type. Examples: 1) For a missing "Accept" header: accepts_content_type("application/json") -> true 2) For an "Accept" header with value "text/plain, application/*": accepts_content_type("application/json") -> true 3) For an "Accept" header with value "text/plain, */*; q=0.0": accepts_content_type("application/json") -> false 4) For an "Accept" header with value "text/plain; q=0.5, */*; q=0.1": accepts_content_type("application/json") -> true 5) For an "Accept" header with value "text/*; q=0.0, */*": accepts_content_type("text/plain") -> false Project: http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/commit/6e0c6598 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/tree/6e0c6598 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/diff/6e0c6598 Branch: refs/heads/import Commit: 6e0c659823f84bbd2b1644a837b26aa0986946c3 Parents: 54aa7e0 Author: Filipe David Manana Authored: Mon Nov 8 14:26:10 2010 +0000 Committer: Filipe David Manana Committed: Mon Nov 8 14:26:10 2010 +0000 ---------------------------------------------------------------------- src/mochiweb_request.erl | 53 ++++++++++++++++++++++++++++ src/mochiweb_request_tests.erl | 63 +++++++++++++++++++++++++++++++++ src/mochiweb_util.erl | 70 +++++++++++++++++++++++++------------ 3 files changed, 163 insertions(+), 23 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/blob/6e0c6598/src/mochiweb_request.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl index 0ee2a19..1bdc7b7 100644 --- a/src/mochiweb_request.erl +++ b/src/mochiweb_request.erl @@ -21,6 +21,7 @@ -export([parse_cookie/0, get_cookie_value/1]). -export([serve_file/2, serve_file/3]). -export([accepted_encodings/1]). +-export([accepts_content_type/1]). -define(SAVE_QS, mochiweb_request_qs). -define(SAVE_PATH, mochiweb_request_path). @@ -708,6 +709,58 @@ accepted_encodings(SupportedEncodings) -> ) end. +%% @spec accepts_content_type(string() | binary()) -> boolean() | bad_accept_header +%% +%% @doc Determines whether a request accepts a given media type by analyzing its +%% its "Accept" header. +%% +%% Examples +%% +%% 1) For a missing "Accept" header: +%% accepts_content_type("application/json") -> true +%% +%% 2) For an "Accept" header with value "text/plain, application/*": +%% accepts_content_type("application/json") -> true +%% +%% 3) For an "Accept" header with value "text/plain, */*; q=0.0": +%% accepts_content_type("application/json") -> false +%% +%% 4) For an "Accept" header with value "text/plain; q=0.5, */*; q=0.1": +%% accepts_content_type("application/json") -> true +%% +%% 5) For an "Accept" header with value "text/*; q=0.0, */*": +%% accepts_content_type("text/plain") -> false +%% +accepts_content_type(ContentType) when is_binary(ContentType) -> + accepts_content_type(binary_to_list(ContentType)); +accepts_content_type(ContentType1) -> + ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]), + AcceptHeader = case get_header_value("Accept") of + undefined -> + "*/*"; + Value -> + Value + end, + case mochiweb_util:parse_qvalues(AcceptHeader) of + invalid_qvalue_string -> + bad_accept_header; + QList -> + [MainType, _SubType] = string:tokens(ContentType, "/"), + SuperType = MainType ++ "/*", + lists:any( + fun({"*/*", Q}) when Q > 0.0 -> + true; + ({Type, Q}) when Q > 0.0 -> + Type =:= ContentType orelse Type =:= SuperType; + (_) -> + false + end, + QList + ) andalso + (not lists:member({ContentType, 0.0}, QList)) andalso + (not lists:member({SuperType, 0.0}, QList)) + end. + %% %% Tests %% http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/blob/6e0c6598/src/mochiweb_request_tests.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb_request_tests.erl b/src/mochiweb_request_tests.erl new file mode 100644 index 0000000..b61a583 --- /dev/null +++ b/src/mochiweb_request_tests.erl @@ -0,0 +1,63 @@ +-module(mochiweb_request_tests). + +-include_lib("eunit/include/eunit.hrl"). +-ifdef(TEST). + +accepts_content_type_test() -> + Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "multipart/related"}])), + ?assertEqual(true, Req1:accepts_content_type("multipart/related")), + + Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html"}])), + ?assertEqual(false, Req2:accepts_content_type("multipart/related")), + + Req3 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, multipart/*"}])), + ?assertEqual(true, Req3:accepts_content_type("multipart/related")), + + Req4 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, multipart/*; q=0.0"}])), + ?assertEqual(false, Req4:accepts_content_type("multipart/related")), + + Req5 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, multipart/*; q=0"}])), + ?assertEqual(false, Req5:accepts_content_type("multipart/related")), + + Req6 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, */*; q=0.0"}])), + ?assertEqual(false, Req6:accepts_content_type("multipart/related")), + + Req7 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "multipart/*; q=0.0, */*"}])), + ?assertEqual(false, Req7:accepts_content_type("multipart/related")), + + Req8 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "*/*; q=0.0, multipart/*"}])), + ?assertEqual(true, Req8:accepts_content_type("multipart/related")), + + Req9 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "*/*; q=0.0, multipart/related"}])), + ?assertEqual(true, Req9:accepts_content_type("multipart/related")), + + Req10 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1"}])), + ?assertEqual(true, Req10:accepts_content_type("text/html;level=1")), + + Req11 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1, text/html"}])), + ?assertEqual(true, Req11:accepts_content_type("text/html")), + + Req12 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1; q=0.0, text/html"}])), + ?assertEqual(false, Req12:accepts_content_type("text/html;level=1")), + + Req13 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1; q=0.0, text/html"}])), + ?assertEqual(false, Req13:accepts_content_type("text/html; level=1")), + + Req14 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html;level=1;q=0.1, text/html"}])), + ?assertEqual(true, Req14:accepts_content_type("text/html; level=1")). + +-endif. http://git-wip-us.apache.org/repos/asf/couchdb-mochiweb/blob/6e0c6598/src/mochiweb_util.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb_util.erl b/src/mochiweb_util.erl index d1cc59d..a22d993 100644 --- a/src/mochiweb_util.erl +++ b/src/mochiweb_util.erl @@ -414,7 +414,8 @@ shell_quote([C | Rest], Acc) -> shell_quote(Rest, [C | Acc]). %% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string -%% @type qvalue() = {encoding(), float()}. +%% @type qvalue() = {media_type() | encoding() , float()}. +%% @type media_type() = string(). %% @type encoding() = string(). %% %% @doc Parses a list (given as a string) of elements with Q values associated @@ -422,7 +423,7 @@ shell_quote([C | Rest], Acc) -> %% from its Q value by a semicolon. Q values are optional but when missing %% the value of an element is considered as 1.0. A Q value is always in the %% range [0.0, 1.0]. A Q value list is used for example as the value of the -%% HTTP "Accept-Encoding" header. +%% HTTP "Accept" and "Accept-Encoding" headers. %% %% Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1). %% @@ -433,29 +434,12 @@ shell_quote([C | Rest], Acc) -> %% parse_qvalues(QValuesStr) -> try - {ok, Re} = re:compile("^\\s*q\\s*=\\s*((?:0|1)(?:\\.\\d{1,3})?)\\s*$"), lists:map( fun(Pair) -> - case string:tokens(Pair, ";") of - [Enc] -> - {string:strip(Enc), 1.0}; - [Enc, QStr] -> - case re:run(QStr, Re, [{capture, [1], list}]) of - {match, [Q]} -> - QVal = case Q of - "0" -> - 0.0; - "1" -> - 1.0; - Else -> - list_to_float(Else) - end, - case QVal < 0.0 orelse QVal > 1.0 of - false -> - {string:strip(Enc), QVal} - end - end - end + [Type | Params] = string:tokens(Pair, ";"), + NormParams = normalize_media_params(Params), + {Q, NonQParams} = extract_q(NormParams), + {string:join([string:strip(Type) | NonQParams], ";"), Q} end, string:tokens(string:to_lower(QValuesStr), ",") ) @@ -464,6 +448,46 @@ parse_qvalues(QValuesStr) -> invalid_qvalue_string end. +normalize_media_params(Params) -> + {ok, Re} = re:compile("\\s"), + normalize_media_params(Re, Params, []). + +normalize_media_params(_Re, [], Acc) -> + lists:reverse(Acc); +normalize_media_params(Re, [Param | Rest], Acc) -> + NormParam = re:replace(Param, Re, "", [global, {return, list}]), + normalize_media_params(Re, Rest, [NormParam | Acc]). + +extract_q(NormParams) -> + {ok, KVRe} = re:compile("^([^=]+)=([^=]+)$"), + {ok, QRe} = re:compile("^((?:0|1)(?:\\.\\d{1,3})?)$"), + extract_q(KVRe, QRe, NormParams, []). + +extract_q(_KVRe, _QRe, [], Acc) -> + {1.0, lists:reverse(Acc)}; +extract_q(KVRe, QRe, [Param | Rest], Acc) -> + case re:run(Param, KVRe, [{capture, [1, 2], list}]) of + {match, [Name, Value]} -> + case Name of + "q" -> + {match, [Q]} = re:run(Value, QRe, [{capture, [1], list}]), + QVal = case Q of + "0" -> + 0.0; + "1" -> + 1.0; + Else -> + list_to_float(Else) + end, + case QVal < 0.0 orelse QVal > 1.0 of + false -> + {QVal, lists:reverse(Acc) ++ Rest} + end; + _ -> + extract_q(KVRe, QRe, Rest, [Param | Acc]) + end + end. + %% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) -> %% [encoding()] %%