diff --git a/rebar.config b/rebar.config index 85cc7362f..ff1e937e1 100644 --- a/rebar.config +++ b/rebar.config @@ -44,8 +44,8 @@ {sext, {git, "https://github.com/OpenRiak/sext.git", {branch, "openriak-3.2"}}}, {riak_pipe, {git, "https://github.com/OpenRiak/riak_pipe.git", {branch, "openriak-3.4"}}}, {riak_dt, {git, "https://github.com/OpenRiak/riak_dt.git", {branch, "openriak-3.2"}}}, - {riak_api, {git, "https://github.com/OpenRiak/riak_api.git", {branch, "openriak-3.4"}}}, + {riak_api, {git, "https://github.com/OpenRiak/riak_api.git", {branch, "nhse-o34-orkv.i30-string"}}}, {hyper, {git, "https://github.com/OpenRiak/hyper", {branch, "openriak-3.2"}}}, {kv_index_tictactree, {git, "https://github.com/OpenRiak/kv_index_tictactree.git", {branch, "openriak-3.4"}}}, - {rhc, {git, "https://github.com/OpenRiak/riak-erlang-http-client", {branch, "openriak-3.4"}}} + {rhc, {git, "https://github.com/OpenRiak/riak-erlang-http-client", {branch, "nhse-o34-nhskv.i30-string"}}} ]}. diff --git a/src/riak_kv_wm_object.erl b/src/riak_kv_wm_object.erl index 8db2d24b4..fa367d8ed 100644 --- a/src/riak_kv_wm_object.erl +++ b/src/riak_kv_wm_object.erl @@ -185,15 +185,13 @@ charset, %% string() | undefined - extracted character set provided timeout, %% integer() - passed-in timeout value in ms security, %% security context - not_modified %% decoded vector clock to be used in not_modified check + not_modified, %% decoded vector clock to be used in not_modified check + header_map %% map of http headers interesting within this module } ). --ifdef(namespaced_types). + -type riak_kv_wm_object_dict() :: dict:dict(). --else. --type riak_kv_wm_object_dict() :: dict(). --endif. -include_lib("webmachine/include/webmachine.hrl"). -include("riak_kv_wm_raw.hrl"). @@ -213,6 +211,72 @@ -define(V2_KEY_REGEX, "; ?riaktag=\"([^\"]+)\""). +-define(BINHEAD_CTYPE, <<"content-type">>). +-define(BINHEAD_IF_NOT_MODIFIED, <<"x-riak-if-not-modified">>). +-define(BINHEAD_NONE_MATCH, <<"if-none-match">>). +-define(BINHEAD_MATCH, <<"if-match">>). +-define(BINHEAD_UNMODIFIED_SINCE, <<"if-unmodified-since">>). +-define(BINHEAD_ENCODING, <<"content-encoding">>). +-define(BINHEAD_ACCEPT, <<"accept">>). +-define(BINHEAD_LINK, <<"link">>). +-define(BINHEAD_VCLOCK, <<"x-riak-vclock">>). +-define(PREFIX_USERMETA, "x-riak-meta-"). +-define(PREFIX_INDEX, "x-riak-index-"). + +-spec make_reqheader_map(request_data()) -> #{binary() => any()}. +make_reqheader_map(RD) -> + lists:foldl( + fun({HeadKey, HeadVal}, AccMap) -> + accumulate_header_info( + list_to_binary(HeadKey), + HeadVal, + AccMap + ) + end, + maps:new(), + mochiweb_headers:to_normalised_list(wrq:req_headers(RD)) + ). + +accumulate_header_info(<>, T, MapAcc) -> + maps:update_with( + ?PREFIX_INDEX, + fun(Indices) -> [{Field, T}|Indices] end, + [{Field, T}], + MapAcc + ); +accumulate_header_info(<>, V, MapAcc) -> + maps:update_with( + ?PREFIX_USERMETA, + fun(Indices) -> + [{<>, V}|Indices] + end, + [{<>, V}], + MapAcc + ); +accumulate_header_info(?BINHEAD_ACCEPT, V, MapAcc) -> + maps:put(?BINHEAD_ACCEPT, V, MapAcc); +accumulate_header_info(?BINHEAD_CTYPE, V, MapAcc) -> + maps:put(?BINHEAD_CTYPE, V, MapAcc); +accumulate_header_info(?BINHEAD_ENCODING, V, MapAcc) -> + maps:put(?BINHEAD_ENCODING, V, MapAcc); +accumulate_header_info(?BINHEAD_VCLOCK, VC, MapAcc) -> + maps:put( + ?BINHEAD_VCLOCK, + riak_object:decode_vclock(base64:decode(VC)), + MapAcc); +accumulate_header_info(?BINHEAD_LINK, V, MapAcc) -> + maps:put(?BINHEAD_LINK, V, MapAcc); +accumulate_header_info(?BINHEAD_IF_NOT_MODIFIED, V, MapAcc) -> + maps:put(?BINHEAD_IF_NOT_MODIFIED, V, MapAcc); +accumulate_header_info(?BINHEAD_UNMODIFIED_SINCE, V, MapAcc) -> + maps:put(?BINHEAD_UNMODIFIED_SINCE, V, MapAcc); +accumulate_header_info(?BINHEAD_MATCH, V, MapAcc) -> + maps:put(?BINHEAD_MATCH, V, MapAcc); +accumulate_header_info(?BINHEAD_NONE_MATCH, V, MapAcc) -> + maps:put(?BINHEAD_NONE_MATCH, V, MapAcc); +accumulate_header_info(_DiscardIdx, _Value, MapAcc) -> + MapAcc. + -spec init(proplists:proplist()) -> {ok, context()}. %% @doc Initialize this resource. This function extracts the %% 'prefix' and 'riak' properties from the dispatch args. @@ -222,8 +286,8 @@ init(Props) -> riak=proplists:get_value(riak, Props), bucket_type=proplists:get_value(bucket_type, Props)}}. --spec service_available(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec service_available(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Determine whether or not a connection to Riak %% can be established. This function also takes this %% opportunity to extract the 'bucket' and 'key' path @@ -250,6 +314,7 @@ service_available(RD, Ctx0=#ctx{riak=RiakProps}) -> list_to_binary( riak_kv_wm_utils:maybe_decode_uri(RD, K)) end, + HeaderMap = make_reqheader_map(RD), {true, RD, Ctx#ctx{ @@ -257,6 +322,7 @@ service_available(RD, Ctx0=#ctx{riak=RiakProps}) -> client=C, bucket=Bucket, key=Key, + header_map = HeaderMap, vtag=wrq:get_qs_value(?Q_VTAG, RD)}}; Error -> {false, @@ -281,7 +347,7 @@ is_authorized(ReqData, Ctx) -> "instead.">>, ReqData), Ctx} end. --spec forbidden(#wm_reqdata{}, context()) -> term(). +-spec forbidden(request_data(), context()) -> term(). forbidden(RD, Ctx) -> case riak_kv_wm_utils:is_forbidden(RD) of true -> @@ -290,7 +356,7 @@ forbidden(RD, Ctx) -> validate(RD, Ctx) end. --spec validate(#wm_reqdata{}, context()) -> term(). +-spec validate(request_data(), context()) -> term(). validate(RD, Ctx=#ctx{security=undefined}) -> validate_resource( RD, Ctx, riak_kv_wm_utils:method_to_perm(Ctx#ctx.method)); @@ -303,7 +369,7 @@ validate(RD, Ctx=#ctx{security=Security}) -> maybe_validate_resource(Res, RD, Ctx, Perm). -spec maybe_validate_resource( - term(), #wm_reqdata{}, context(), string()) -> term(). + term(), request_data(), context(), string()) -> term(). maybe_validate_resource({false, Error, _}, RD, Ctx, _Perm) -> RD1 = wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD), {true, wrq:append_to_resp_body( @@ -312,7 +378,7 @@ maybe_validate_resource({false, Error, _}, RD, Ctx, _Perm) -> maybe_validate_resource({true, _}, RD, Ctx, Perm) -> validate_resource(RD, Ctx, Perm). --spec validate_resource(#wm_reqdata{}, context(), string()) -> term(). +-spec validate_resource(request_data(), context(), string()) -> term(). validate_resource(RD, Ctx, Perm) when Perm == "riak_kv.get" -> %% Ensure the key is here, otherwise 404 %% we do this early as it used to be done in the @@ -343,21 +409,21 @@ validate_bucket_type(RD, Ctx) -> handle_common_error(bucket_type_unknown, RD, Ctx) end. --spec allowed_methods(#wm_reqdata{}, context()) -> - {[atom()], #wm_reqdata{}, context()}. +-spec allowed_methods(request_data(), context()) -> + {[atom()], request_data(), context()}. %% @doc Get the list of methods this resource supports. allowed_methods(RD, Ctx) -> {['HEAD', 'GET', 'POST', 'PUT', 'DELETE'], RD, Ctx}. --spec allow_missing_post(#wm_reqdata{}, context()) -> - {true, #wm_reqdata{}, context()}. +-spec allow_missing_post(request_data(), context()) -> + {true, request_data(), context()}. %% @doc Makes POST and PUT equivalent for creating new %% bucket entries. allow_missing_post(RD, Ctx) -> {true, RD, Ctx}. --spec malformed_request(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec malformed_request(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Determine whether query parameters, request headers, %% and request body are badly-formed. %% Body format is checked to be valid JSON, including @@ -403,7 +469,7 @@ malformed_request([H|T], RD, Ctx) -> %% PUT/POST. %% This should probably result in a 415 using the known_content_type callback malformed_content_type(RD, Ctx) -> - case wrq:get_req_header(?HEAD_CTYPE, RD) of + case maps:get(?BINHEAD_CTYPE, Ctx#ctx.header_map, undefined) of undefined -> {true, missing_content_type(RD), Ctx}; RawCType -> @@ -413,8 +479,8 @@ malformed_content_type(RD, Ctx) -> {false, RD, Ctx#ctx{ctype = ContentType, charset = Charset}} end. --spec malformed_timeout_param(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec malformed_timeout_param(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Check that the timeout parameter is are a %% string-encoded integer. Store the integer value %% in context() if so. @@ -438,8 +504,8 @@ malformed_timeout_param(RD, Ctx) -> end end. --spec malformed_rw_params(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec malformed_rw_params(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Check that r, w, dw, and rw query parameters are %% string-encoded integers. Store the integer values %% in context() if so. @@ -447,96 +513,103 @@ malformed_rw_params(RD, Ctx) -> Res = lists:foldl(fun malformed_rw_param/2, {false, RD, Ctx}, - [{#ctx.r, "r", "default"}, - {#ctx.w, "w", "default"}, - {#ctx.dw, "dw", "default"}, - {#ctx.rw, "rw", "default"}, - {#ctx.pw, "pw", "default"}, - {#ctx.node_confirms, "node_confirms", "default"}, - {#ctx.pr, "pr", "default"}]), + [{#ctx.r, "r", default}, + {#ctx.w, "w", default}, + {#ctx.dw, "dw", default}, + {#ctx.rw, "rw", default}, + {#ctx.pw, "pw", default}, + {#ctx.node_confirms, "node_confirms", default}, + {#ctx.pr, "pr", default}]), Res2 = lists:foldl(fun malformed_custom_param/2, Res, [{#ctx.sync_on_write, "sync_on_write", - "default", + default, [default, backend, one, all]}]), lists:foldl(fun malformed_boolean_param/2, Res2, - [{#ctx.basic_quorum, "basic_quorum", "default"}, - {#ctx.notfound_ok, "notfound_ok", "default"}, - {#ctx.asis, "asis", "false"}]). + [{#ctx.basic_quorum, "basic_quorum", default}, + {#ctx.notfound_ok, "notfound_ok", default}, + {#ctx.asis, "asis", false}]). --spec malformed_rw_param({Idx::integer(), Name::string(), Default::string()}, - {boolean(), #wm_reqdata{}, context()}) -> - {boolean(), #wm_reqdata{}, context()}. +-spec malformed_rw_param({Idx::integer(), Name::string(), Default::atom()}, + {boolean(), request_data(), context()}) -> + {boolean(), request_data(), context()}. %% @doc Check that a specific r, w, dw, or rw query param is a %% string-encoded integer. Store its result in context() if it %% is, or print an error message in #wm_reqdata{} if it is not. malformed_rw_param({Idx, Name, Default}, {Result, RD, Ctx}) -> - case catch normalize_rw_param(wrq:get_qs_value(Name, Default, RD)) of - P when (is_atom(P) orelse is_integer(P)) -> - {Result, RD, setelement(Idx, Ctx, P)}; - _ -> - {true, - wrq:append_to_resp_body( - io_lib:format("~s query parameter must be an integer or " - "one of the following words: 'one', 'quorum' or 'all'~n", - [Name]), - wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)), - Ctx} + case wrq:get_qs_value(Name, RD) of + undefined -> + {Result, RD, setelement(Idx, Ctx, Default)}; + ExtractedString -> + case catch normalize_rw_param(ExtractedString) of + P when (is_atom(P) orelse is_integer(P)) -> + {Result, RD, setelement(Idx, Ctx, P)}; + _ -> + {true, + wrq:append_to_resp_body( + io_lib:format("~s query parameter must be an integer or " + "one of the following words: 'one', 'quorum' or 'all'~n", + [Name]), + wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)), + Ctx} + end end. -spec malformed_custom_param({Idx::integer(), Name::string(), - Default::string(), + Default::atom(), AllowedValues::[atom()]}, - {boolean(), #wm_reqdata{}, context()}) -> - {boolean(), #wm_reqdata{}, context()}. + {boolean(), request_data(), context()}) -> + {boolean(), request_data(), context()}. %% @doc Check that a custom parameter is one of the AllowedValues %% Store its result in context() if it is, or print an error message %% in #wm_reqdata{} if it is not. malformed_custom_param({Idx, Name, Default, AllowedValues}, {Result, RD, Ctx}) -> - AllowedValueTuples = [{V} || V <- AllowedValues], - Option= - lists:keyfind( - list_to_atom( - string:to_lower( - wrq:get_qs_value(Name, Default, RD))), - 1, - AllowedValueTuples), - case Option of - false -> - ErrorText = - "~s query parameter must be one of the following words: ~p~n", - {true, - wrq:append_to_resp_body( - io_lib:format(ErrorText, [Name, AllowedValues]), - wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)), - Ctx}; - _ -> - {Value} = Option, - {Result, RD, setelement(Idx, Ctx, Value)} + case wrq:get_qs_value(Name, RD) of + undefined -> + {Result, RD, setelement(Idx, Ctx, Default)}; + ExtractedString -> + UsableValue = list_to_atom(string:lowercase(ExtractedString)), + case lists:member(UsableValue, AllowedValues) of + true -> + {Result, RD, setelement(Idx, Ctx, UsableValue)}; + false -> + ErrorText = + "~s query parameter must be one of the following words: ~p~n", + {true, + wrq:append_to_resp_body( + io_lib:format(ErrorText, [Name, AllowedValues]), + wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)), + Ctx} + end end. %% @doc Check that a specific query param is a %% string-encoded boolean. Store its result in context() if it %% is, or print an error message in #wm_reqdata{} if it is not. malformed_boolean_param({Idx, Name, Default}, {Result, RD, Ctx}) -> - case string:to_lower(wrq:get_qs_value(Name, Default, RD)) of - "true" -> - {Result, RD, setelement(Idx, Ctx, true)}; - "false" -> - {Result, RD, setelement(Idx, Ctx, false)}; - "default" -> - {Result, RD, setelement(Idx, Ctx, default)}; - _ -> - {true, - wrq:append_to_resp_body( - io_lib:format("~s query parameter must be true or false~n", - [Name]), - wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)), - Ctx} + case wrq:get_qs_value(Name, RD) of + undefined -> + {Result, RD, setelement(Idx, Ctx, Default)}; + ExtractedString -> + case string:lowercase(ExtractedString) of + "true" -> + {Result, RD, setelement(Idx, Ctx, true)}; + "false" -> + {Result, RD, setelement(Idx, Ctx, false)}; + "default" -> + {Result, RD, setelement(Idx, Ctx, default)}; + _ -> + {true, + wrq:append_to_resp_body( + io_lib:format("~s query parameter must be true or false~n", + [Name]), + wrq:set_resp_header(?HEAD_CTYPE, "text/plain", RD)), + Ctx} + end end. normalize_rw_param("backend") -> backend; @@ -546,15 +619,15 @@ normalize_rw_param("quorum") -> quorum; normalize_rw_param("all") -> all; normalize_rw_param(V) -> list_to_integer(V). --spec malformed_link_headers(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec malformed_link_headers(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Check that the Link header in the request() is valid. %% Store the parsed links in context() if the header is valid, %% or print an error in #wm_reqdata{} if it is not. %% A link header should be of the form: %% </Prefix/Bucket/Key>; riaktag="Tag",... malformed_link_headers(RD, Ctx) -> - case catch get_link_heads(RD, Ctx) of + case catch get_link_heads(Ctx) of Links when is_list(Links) -> {false, RD, Ctx#ctx{links=Links}}; _Error when Ctx#ctx.api_version == 1-> @@ -575,15 +648,15 @@ malformed_link_headers(RD, Ctx) -> end. --spec malformed_index_headers(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec malformed_index_headers(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Check that the Index headers (HTTP headers prefixed with index_") %% are valid. Store the parsed headers in context() if valid, %% or print an error in #wm_reqdata{} if not. %% An index field should be of the form "index_fieldname_type" malformed_index_headers(RD, Ctx) -> %% Get a list of index_headers... - IndexFields1 = extract_index_fields(RD), + IndexFields1 = extract_index_fields(Ctx), %% Validate the fields. If validation passes, then the index %% headers are correctly formed. @@ -598,37 +671,23 @@ malformed_index_headers(RD, Ctx) -> Ctx} end. --spec extract_index_fields(#wm_reqdata{}) -> proplists:proplist(). +-spec extract_index_fields(context()) -> proplists:proplist(). %% @doc Extract fields from headers prefixed by "x-riak-index-" in the %% client's PUT request, to be indexed at write time. -extract_index_fields(RD) -> - PrefixSize = length(?HEAD_INDEX_PREFIX), - {ok, RE} = re:compile(",\\s"), - F = - fun({K,V}, Acc) -> - KList = riak_kv_wm_utils:any_to_list(K), - case lists:prefix(?HEAD_INDEX_PREFIX, string:to_lower(KList)) of - true -> - %% Isolate the name of the index field. - IndexField = - list_to_binary( - element(2, lists:split(PrefixSize, KList))), - - %% HACK ALERT: Split values on comma. The HTTP - %% spec allows for comma separated tokens - %% where the tokens can be quoted strings. We - %% don't currently support quoted strings. - %% (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html) - Values = re:split(V, RE, [{return, binary}]), - [{IndexField, X} || X <- Values] ++ Acc; - false -> - Acc - end - end, - lists:foldl(F, [], mochiweb_headers:to_list(wrq:req_headers(RD))). +extract_index_fields(Ctx) -> + RE = get_compiled_index_regex(), + lists:flatten( + lists:map( + fun({Field, Term}) -> + Values = re:split(Term, RE, [{return, binary}]), + [{Field, X} || X <- Values] + end, + maps:get(?PREFIX_INDEX, Ctx#ctx.header_map, []) + ) + ). --spec content_types_provided(#wm_reqdata{}, context()) -> - {[{ContentType::string(), Producer::atom()}], #wm_reqdata{}, context()}. +-spec content_types_provided(request_data(), context()) -> + {[{ContentType::string(), Producer::atom()}], request_data(), context()}. %% @doc List the content types available for representing this resource. %% The content-type for a key-level request is the content-type that %% was used in the PUT request that stored the document in Riak. @@ -649,9 +708,9 @@ content_types_provided(RD, Ctx0) -> {"multipart/mixed", produce_multipart_body}], RD, DocCtx} end. --spec charsets_provided(#wm_reqdata{}, context()) -> +-spec charsets_provided(request_data(), context()) -> {no_charset|[{Charset::string(), Producer::function()}], - #wm_reqdata{}, context()}. + request_data(), context()}. %% @doc List the charsets available for representing this resource. %% The charset for a key-level request is the charset that was used %% in the PUT request that stored the document in Riak (none if @@ -686,14 +745,20 @@ charsets_provided(RD, Ctx0) -> {no_charset, RD, DocCtx} end. --spec encodings_provided(#wm_reqdata{}, context()) -> - {[{Encoding::string(), Producer::function()}], #wm_reqdata{}, context()}. +-spec encodings_provided(request_data(), context()) -> + {[{Encoding::string(), Producer::function()}], request_data(), context()}. %% @doc List the encodings available for representing this resource. %% The encoding for a key-level request is the encoding that was %% used in the PUT request that stored the document in Riak, or %% "identity" and "gzip" if no encoding was specified at PUT-time. encodings_provided(RD, Ctx0) -> - DocCtx = ensure_doc(RD, Ctx0), + DocCtx = + case Ctx0#ctx.method of + UpdM when UpdM =:= 'PUT'; UpdM =:= 'POST'; UpdM =:= 'DELETE' -> + Ctx0; + _ -> + ensure_doc(RD, Ctx0) + end, case DocCtx#ctx.doc of {ok, _} -> case select_doc(DocCtx) of @@ -711,15 +776,15 @@ encodings_provided(RD, Ctx0) -> {riak_kv_wm_utils:default_encodings(), RD, DocCtx} end. --spec content_types_accepted(#wm_reqdata{}, context()) -> +-spec content_types_accepted(request_data(), context()) -> {[{ContentType::string(), Acceptor::atom()}], - #wm_reqdata{}, context()}. + request_data(), context()}. %% @doc Get the list of content types this resource will accept. %% Whatever content type is specified by the Content-Type header %% of a key-level PUT request will be accepted by this resource. %% (A key-level put *must* include a Content-Type header.) content_types_accepted(RD, Ctx) -> - case wrq:get_req_header(?HEAD_CTYPE, RD) of + case maps:get(?BINHEAD_CTYPE, Ctx#ctx.header_map, undefined) of undefined -> %% user must specify content type of the data {[], RD, Ctx}; @@ -743,14 +808,22 @@ content_types_accepted(RD, Ctx) -> end end. --spec resource_exists(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec resource_exists(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Determine whether or not the requested item exists. %% Documents exists if a read request to Riak returns {ok, riak_object()}, %% and either no vtag query parameter was specified, or the value of the %% vtag param matches the vtag of some value of the Riak object. -resource_exists(RD, Ctx0) -> - case element(1, doc_required(RD, Ctx0)) of +resource_exists(RD, Ctx0) -> + Method = Ctx0#ctx.method, + ToFetch = + case Method of + UpdM when UpdM =:= 'PUT'; UpdM =:= 'POST'; UpdM =:= 'DELETE' -> + conditional_headers_present(Ctx0) == true; + _ -> + true + end, + case ToFetch of true -> DocCtx = ensure_doc(RD, Ctx0), case DocCtx#ctx.doc of @@ -798,7 +871,9 @@ doc_required(RD, Context) -> -spec is_conflict(request_data(), context()) -> {boolean(), request_data(), context()}. is_conflict(RD, Ctx) -> - case {Ctx#ctx.method, wrq:get_req_header(?HEAD_IF_NOT_MODIFIED, RD)} of + NotModified = + maps:get(?BINHEAD_IF_NOT_MODIFIED, Ctx#ctx.header_map, undefined), + case {Ctx#ctx.method, NotModified} of {_ , undefined} -> {false, RD, Ctx}; {UpdM, NotModifiedClock} when UpdM =:= 'PUT'; UpdM =:= 'POST' -> @@ -820,21 +895,17 @@ is_conflict(RD, Ctx) -> {false, RD, Ctx} end. --spec conditional_headers_present(request_data()) -> boolean(). -conditional_headers_present(RD) -> - NoneMatch = - (wrq:get_req_header("If-None-Match", RD) =/= undefined), - Match = - (wrq:get_req_header("If-Match", RD) =/= undefined), - UnModifiedSince = - (wrq:get_req_header("If-Unmodified-Since", RD) =/= undefined), - NotModified = - (wrq:get_req_header(?HEAD_IF_NOT_MODIFIED, RD) =/= undefined), +-spec conditional_headers_present(context()) -> boolean(). +conditional_headers_present(Ctx) -> + NoneMatch = maps:is_key(?BINHEAD_NONE_MATCH, Ctx#ctx.header_map), + Match = maps:is_key(?BINHEAD_MATCH, Ctx#ctx.header_map), + UnModifiedSince = maps:is_key(?BINHEAD_UNMODIFIED_SINCE, Ctx#ctx.header_map), + NotModified = maps:is_key(?BINHEAD_IF_NOT_MODIFIED, Ctx#ctx.header_map), (NoneMatch or Match or UnModifiedSince or NotModified). --spec post_is_create(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec post_is_create(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc POST is considered a document-creation operation for bucket-level %% requests (this makes webmachine call create_path/2, where the key %% for the created document will be chosen). @@ -845,8 +916,8 @@ post_is_create(RD, Ctx) -> %% key-POST is not create {false, RD, Ctx}. --spec create_path(#wm_reqdata{}, context()) -> - {string(), #wm_reqdata{}, context()}. +-spec create_path(request_data(), context()) -> + {string(), request_data(), context()}. %% @doc Choose the Key for the document created during a bucket-level POST. %% This function also sets the Location header to generate a %% 201 Created response. @@ -858,14 +929,14 @@ create_path(RD, Ctx=#ctx{prefix=P, bucket_type=T, bucket=B, api_version=V}) -> RD), Ctx#ctx{key=list_to_binary(K)}}. --spec process_post(#wm_reqdata{}, context()) -> - {true, #wm_reqdata{}, context()}. +-spec process_post(request_data(), context()) -> + {true, request_data(), context()}. %% @doc Pass-through for key-level requests to allow POST to function %% as PUT for clients that do not support PUT. process_post(RD, Ctx) -> accept_doc_body(RD, Ctx). --spec accept_doc_body(#wm_reqdata{}, context()) -> - {true, #wm_reqdata{}, context()}. +-spec accept_doc_body(request_data(), context()) -> + {true, request_data(), context()}. %% @doc Store the data the client is PUTing in the document. %% This function translates the headers and body of the HTTP request %% into their final riak_object() form, and executes the Riak put. @@ -878,8 +949,12 @@ accept_doc_body( not_modified = IfNotModified }) -> Doc0 = riak_object:new(riak_kv_wm_utils:maybe_bucket_type(T,B), K, <<>>), - VclockDoc = riak_object:set_vclock(Doc0, decode_vclock_header(RD)), - UserMeta = extract_user_meta(RD), + VclockDoc = + riak_object:set_vclock( + Doc0, + maps:get(?BINHEAD_VCLOCK, Ctx#ctx.header_map, vclock:fresh()) + ), + UserMeta = maps:get(?PREFIX_USERMETA, Ctx#ctx.header_map, []), CTypeMD = dict:store(?MD_CTYPE, CType, dict:new()), CharsetMD = if Charset /= undefined -> @@ -888,7 +963,7 @@ accept_doc_body( CTypeMD end, EncMD = - case wrq:get_req_header(?HEAD_ENCODING, RD) of + case maps:get(?BINHEAD_ENCODING, Ctx#ctx.header_map, undefined) of undefined -> CharsetMD; E -> dict:store(?MD_ENCODING, E, CharsetMD) end, @@ -905,7 +980,7 @@ accept_doc_body( _ -> [] end, Options = make_options(Options0, Ctx), - IfNoneMatch = (wrq:get_req_header("If-None-Match", RD) =/= undefined), + IfNoneMatch = maps:is_key(?BINHEAD_NONE_MATCH, Ctx#ctx.header_map), IsConsistent = riak_kv_util:consistent_object(B), CondPutMode = application:get_env(riak_kv, conditional_put_mode, api_only), @@ -986,7 +1061,7 @@ send_returnbody(RD, DocCtx, _HasSiblings = false) -> %% Handle the sibling case. Send either the sibling message body, or a %% multipart body, depending on what the client accepts. send_returnbody(RD, DocCtx, _HasSiblings = true) -> - AcceptHdr = wrq:get_req_header("Accept", RD), + AcceptHdr = maps:get(?BINHEAD_ACCEPT, DocCtx#ctx.header_map, undefined), PossibleTypes = ["multipart/mixed", "text/plain"], case webmachine_util:choose_media_type(PossibleTypes, AcceptHdr) of "multipart/mixed" -> @@ -1015,19 +1090,8 @@ add_conditional_headers(RD, Ctx) -> calendar:universal_time_to_local_time(LM)), RD4), {RD5,Ctx3}. --spec extract_user_meta(#wm_reqdata{}) -> proplists:proplist(). -%% @doc Extract headers prefixed by X-Riak-Meta- in the client's PUT request -%% to be returned by subsequent GET requests. -extract_user_meta(RD) -> - lists:filter(fun({K,_V}) -> - lists:prefix( - ?HEAD_USERMETA_PREFIX, - string:to_lower(riak_kv_wm_utils:any_to_list(K))) - end, - mochiweb_headers:to_list(wrq:req_headers(RD))). - --spec multiple_choices(#wm_reqdata{}, context()) -> - {boolean(), #wm_reqdata{}, context()}. +-spec multiple_choices(request_data(), context()) -> + {boolean(), request_data(), context()}. %% @doc Determine whether a document has siblings. If the user has %% specified a specific vtag, the document is considered not to %% have sibling versions. This is a safe assumption, because @@ -1066,8 +1130,8 @@ multiple_choices(RD, Ctx) -> multiple_choices}) end. --spec produce_doc_body(#wm_reqdata{}, context()) -> - {binary(), #wm_reqdata{}, context()}. +-spec produce_doc_body(request_data(), context()) -> + {binary(), request_data(), context()}. %% @doc Extract the value of the document, and place it in the %% response body of the request. This function also adds the %% Link, X-Riak-Meta- headers, and X-Riak-Index- headers to the @@ -1126,8 +1190,8 @@ produce_doc_body(RD, Ctx) -> multiple_choices}) end. --spec produce_sibling_message_body(#wm_reqdata{}, context()) -> - {iolist(), #wm_reqdata{}, context()}. +-spec produce_sibling_message_body(request_data(), context()) -> + {iolist(), request_data(), context()}. %% @doc Produce the text message informing the user that there are multiple %% values for this document, and giving that user the vtags of those %% values so they can get to them with the vtag query param. @@ -1139,8 +1203,8 @@ produce_sibling_message_body(RD, Ctx=#ctx{doc={ok, Doc}}) -> encode_vclock_header(RD, Ctx)), Ctx}. --spec produce_multipart_body(#wm_reqdata{}, context()) -> - {iolist(), #wm_reqdata{}, context()}. +-spec produce_multipart_body(request_data(), context()) -> + {iolist(), request_data(), context()}. %% @doc Produce a multipart body representation of an object with multiple %% values (siblings), each sibling being one part of the larger %% document. @@ -1183,7 +1247,7 @@ select_doc(#ctx{doc={ok, Doc}, vtag=Vtag}) -> {riak_object:get_update_metadata(Doc), UpdateValue} end. --spec encode_vclock_header(#wm_reqdata{}, context()) -> #wm_reqdata{}. +-spec encode_vclock_header(request_data(), context()) -> request_data(). %% @doc Add the X-Riak-Vclock header to the response. encode_vclock_header(RD, #ctx{doc={ok, Doc}}) -> {Head, Val} = riak_object:vclock_header(Doc), @@ -1193,16 +1257,6 @@ encode_vclock_header(RD, #ctx{doc={error, {deleted, VClock}}}) -> wrq:set_resp_header( ?HEAD_VCLOCK, binary_to_list(base64:encode(BinVClock)), RD). --spec decode_vclock_header(#wm_reqdata{}) -> vclock:vclock(). -%% @doc Translate the X-Riak-Vclock header value from the request into -%% its Erlang representation. If no vclock header exists, a fresh -%% vclock is returned. -decode_vclock_header(RD) -> - case wrq:get_req_header(?HEAD_VCLOCK, RD) of - undefined -> vclock:fresh(); - Head -> riak_object:decode_vclock(base64:decode(Head)) - end. - -spec ensure_doc(request_data(), context()) -> context(). %% @doc Ensure that the 'doc' field of the context() has been filled %% with the result of a riak_client:get request. This is a @@ -1235,18 +1289,17 @@ ensure_doc(RD, Ctx=#ctx{doc=undefined, bucket_type=T, bucket=B, key=K, client=C, end; ensure_doc(_RD, Ctx) -> Ctx. --spec delete_resource(#wm_reqdata{}, context()) -> - {true, #wm_reqdata{}, context()}. +-spec delete_resource(request_data(), context()) -> + {true, request_data(), context()}. %% @doc Delete the document specified. delete_resource(RD, Ctx=#ctx{bucket_type=T, bucket=B, key=K, client=C}) -> Options = make_options([], Ctx), BT = riak_kv_wm_utils:maybe_bucket_type(T,B), Result = - case wrq:get_req_header(?HEAD_VCLOCK, RD) of + case maps:get(?BINHEAD_VCLOCK, Ctx#ctx.header_map, undefined) of undefined -> riak_client:delete(BT, K, Options, C); - _ -> - VC = decode_vclock_header(RD), + VC -> riak_client:delete_vclock(BT, K, VC, Options, C) end, case Result of @@ -1264,8 +1317,8 @@ md5(Bin) -> crypto:md5(Bin). -endif. --spec generate_etag(#wm_reqdata{}, context()) -> - {undefined|string(), #wm_reqdata{}, context()}. +-spec generate_etag(request_data(), context()) -> + {undefined|string(), request_data(), context()}. %% @doc Get the etag for this resource. %% Documents will have an etag equal to their vtag. For documents with %% siblings when no vtag is specified, this will be an etag derived from @@ -1281,8 +1334,8 @@ generate_etag(RD, Ctx) -> {riak_core_util:integer_to_list(ETag, 62), RD, Ctx} end. --spec last_modified(#wm_reqdata{}, context()) -> - {undefined|calendar:datetime(), #wm_reqdata{}, context()}. +-spec last_modified(request_data(), context()) -> + {undefined|calendar:datetime(), request_data(), context()}. %% @doc Get the last-modified time for this resource. %% Documents will have the last-modified time specified by the %% riak_object. @@ -1310,18 +1363,18 @@ normalize_last_modified(MD) -> httpd_util:convert_request_date(Rfc1123) end. --spec get_link_heads(#wm_reqdata{}, context()) -> [link()]. +-spec get_link_heads(context()) -> [link()]. %% @doc Extract the list of links from the Link request header. %% This function will die if an invalid link header format %% is found. -get_link_heads(RD, Ctx) -> +get_link_heads(Ctx) -> APIVersion = Ctx#ctx.api_version, Prefix = Ctx#ctx.prefix, Bucket = Ctx#ctx.bucket, %% Get a list of link headers... - LinkHeaders1 = - case wrq:get_req_header(?HEAD_LINK, RD) of + LinkHeaders = + case maps:get(?BINHEAD_LINK, Ctx#ctx.header_map, undefined) of undefined -> []; Heads -> string:tokens(Heads, ",") end, @@ -1329,21 +1382,14 @@ get_link_heads(RD, Ctx) -> %% Decode the link headers. Throw an exception if we can't %% properly parse any of the headers... {BucketLinks, KeyLinks} = - case APIVersion of - 1 -> - {ok, BucketRegex} = - re:compile("= 2 -> - {ok, BucketRegex} = - re:compile(?V2_BUCKET_REGEX), - {ok, KeyRegex} = - re:compile(?V2_KEY_REGEX), - extract_links(LinkHeaders1, BucketRegex, KeyRegex) - end, + case LinkHeaders of + [] -> + {[], []}; + LinkHeaders -> + {KeyRegex, BucketRegex} = + get_compiled_link_regex(APIVersion, Prefix), + extract_links(LinkHeaders, BucketRegex, KeyRegex) + end, %% Validate that the only bucket header is pointing to the parent %% bucket... @@ -1352,7 +1398,7 @@ get_link_heads(RD, Ctx) -> true -> KeyLinks; false -> - throw({invalid_link_headers, LinkHeaders1}) + throw({invalid_link_headers, LinkHeaders}) end. %% Run each LinkHeader string() through the BucketRegex and @@ -1382,6 +1428,50 @@ extract_links_1([LinkHeader|Rest], BucketRegex, KeyRegex, BucketAcc, KeyAcc) -> extract_links_1([], _BucketRegex, _KeyRegex, BucketAcc, KeyAcc) -> {BucketAcc, KeyAcc}. +-type mp() :: {re_pattern, _, _, _, _}. + +-spec get_compiled_link_regex(non_neg_integer(), string()) -> {mp(), mp()}. +get_compiled_link_regex(1, Prefix) -> + case persistent_term:get(compiled_link_regex_v1, undefined) of + undefined -> + {ok, KeyRegex} = re:compile(" + PreCompiledExpressions + end; +get_compiled_link_regex(Two, _Prefix) when Two >= 2 -> + case persistent_term:get(compiled_link_regex_v2, undefined) of + undefined -> + {ok, KeyRegex} = re:compile(?V2_KEY_REGEX), + {ok, BucketRegex} = re:compile(?V2_BUCKET_REGEX), + persistent_term:put( + compiled_link_regex_v2, + {KeyRegex, BucketRegex} + ), + {KeyRegex, BucketRegex}; + PreCompiledExpressions -> + PreCompiledExpressions + end. + +-spec get_compiled_index_regex() -> mp(). +get_compiled_index_regex() -> + case persistent_term:get(compiled_index_regex, undefined) of + undefined -> + {ok, IndexRegex} = re:compile(",\\s"), + persistent_term:put( + compiled_index_regex, + IndexRegex + ), + IndexRegex; + PreCompiledIndexRegex -> + PreCompiledIndexRegex + end. + -spec get_ctype(riak_kv_wm_object_dict(), term()) -> string(). %% @doc Work out the content type for this object - use the metadata if provided get_ctype(MD,V) -> @@ -1512,4 +1602,4 @@ make_options(Prev, Ctx) -> ensure_bucket_type(RD, Ctx) -> Ctx0 = riak_kv_wm_utils:ensure_bucket_type(RD, Ctx, #ctx.bucket_type), Ctx0#ctx{type_exists = - riak_kv_wm_utils:bucket_type_exists(Ctx0#ctx.bucket_type)}. + riak_kv_wm_utils:bucket_type_exists(Ctx0#ctx.bucket_type)}. \ No newline at end of file