src/cowmachine_decision_core.erl

%% @author Justin Sheehy <justin@basho.com>
%% @author Andy Gross <andy@basho.com>
%% @author Bryan Fink <bryan@basho.com>
%% @copyright 2007-2009 Basho Technologies
%% @end
%%
%%    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 HTTP decision core for cowmachine

-module(cowmachine_decision_core).

-author('Justin Sheehy <justin@basho.com>').
-author('Andy Gross <andy@basho.com>').
-author('Bryan Fink <bryan@basho.com>').
-author('Marc Worrell <marc@worrell.nl>').

-export([handle_request/2]).

-export([respond/3]).

-include("cowmachine_state.hrl").
-include("cowmachine_log.hrl").

-export_type([cmstate/0]).

%% @doc Handle Cowmachine state request.
%% @throws {stop_request, Code, Reason}

-spec handle_request(CmState, Context) -> Result when
	CmState :: cmstate(), 
	Context :: cowmachine_req:context(),
	Result :: {atom(), StateResult, Context} | {upgrade, UpgradeFun, StateResult, Context},
	StateResult :: CmState,
	UpgradeFun :: atom().
handle_request(#cmstate{ controller = Controller } = CmState, Context) ->
    code:ensure_loaded(Controller),
    d(v3b13, CmState, Context).

%% @doc Call the controller

-spec controller_call(Callback, State, Context) -> Result when
	Callback :: atom(),
	State :: cmstate(),
	Context :: term(),
	Result :: {term(), #cmstate{}, term()}.
controller_call(Callback, #cmstate{cache=Cache} = State, Context) ->
    case is_cacheable(Callback) of
        true ->
            case maps:find(Callback, Cache) of
                error -> 
                    {T, Context1} = cowmachine_controller:do(Callback, State, Context),
                    State1 = State#cmstate{cache = Cache#{Callback => T}},
                    {T, State1, Context1};
                {ok, Cached} -> 
                    {Cached, State, Context}
            end;
        false ->
            {T, Context1} = cowmachine_controller:do(Callback, State, Context),
            {T, State, Context1}
    end.

-spec is_cacheable(Callback) -> Result when
	Callback :: charsets_provided | content_types_provided | content_encodings_provided |
				last_modified | generate_etag | any(),
	Result :: boolean().			
is_cacheable(charsets_provided) -> true;
is_cacheable(content_types_provided) -> true;
is_cacheable(content_encodings_provided) -> true;
is_cacheable(last_modified) -> true;
is_cacheable(generate_etag) -> true;
is_cacheable(_) -> false.

-spec controller_call_process(ContentType, State, Context) -> Result when
	ContentType :: cow_http_hd:media_type(),
	State :: cmstate(),
	Context :: cowmachine_req:context(),
	Result :: {Res, State, Context},
    Res :: boolean() | cowmachine_req:halt() | {error, any(), any()} | {error, any()} |
            cowmachine_req:resp_body().
controller_call_process(ContentType, State, Context) ->
    {T, Context1} = cowmachine_controller:do_process(ContentType, State, Context),
    {T, State, Context1}.

-spec d(DecisionID, State, Context) -> Result when
	DecisionID :: v3b13 |	v3b12 |	v3b11 |	v3b10 |	v3b9 |	v3b9a |	v3b9b |	v3b8 |	v3b7 |	v3b6_upgrade |	v3b6 |	v3b5 |	v3b4 |	v3b3 |	v3c3 |	v3c4 |	v3d4 |	v3d5 |	v3e5 |	v3e6 |	v3f6 |	v3f7 |	v3g7 |	v3g8 |	v3g9 |	v3g11 |	v3h7 |	v3h10 |	v3h11 |	v3h12 |	v3i4 |	v3i7 |	v3i12 |	v3i13 |	v3j18 |	v3k5 |	v3k7 |	v3k13 |	v3l5 |	v3l7 |	v3l13 |	v3l14 |	v3l15 |	v3l17 |	v3m5 |	v3m7 |	v3m16 |	v3m20 |	v3m20b |	v3n5 |	v3n11 |	v3n16 |	v3o14 |	v3o16 |	v3o18 |	v3o18b |	v3o20 |	v3p3 |	v3p11, 
	State :: cmstate(), 
	Context :: cowmachine_req:context(),
	Result :: {atom(), StateResult, Context} | {upgrade, UpgradeFun, StateResult, Context},
	StateResult :: State,
	UpgradeFun :: atom().
d(DecisionID, State, Context) ->
    % webmachine_controller:log_d(Rs, DecisionID),
    decision(DecisionID, State, Context).

%% @doc Cowmachine response.
%% @throws {stop_request, Code}

-spec respond(Code, State, Context) -> Result when
	Code :: integer(), 
	State :: cmstate(), 
	Context :: cowmachine_req:context(),
	%Result :: {State, Context}.
	Result :: {term(), #cmstate{}, term()}.
respond(Code, State, Context) ->
    {State1, Context1} = case Code of
        Ok when Ok >= 200, Ok =< 299 ->
            % Response all ok
            {State, Context};
        301 ->
            % Permanent redirect
            {State, Context};
        R when R =:= 303; R =:= 307 ->
            % Temp redirects have no content and should not be cached
            CtxNoCT = cowmachine_req:remove_resp_header(<<"content-type">>, Context),
            {Etag, StateEt, CtxEt0} = controller_call(generate_etag, State, CtxNoCT),
            CtxEt = case Etag of
                undefined -> CtxEt0;
                ETag -> cowmachine_req:set_resp_header(<<"etag">>, cow_uri:urlencode(ETag), CtxEt0)
            end,
            {Expires, StateExp, ExpCtx0} = controller_call(expires, StateEt, CtxEt),
            ExpCtx = case Expires of
                undefined ->
                    ExpCtx0;
                Exp ->
                    cowmachine_req:set_resp_header(
                                    <<"expires">>,
                                    z_convert:to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Exp))),
                                    ExpCtx0)
            end,
            {StateExp, ExpCtx};
        E when E =:= 401; E =:= 403; E =:= 404; E=:= 410; (E >= 500 andalso E =< 599) ->
            % Http errors - maybe handled by error controller
            HasRespContentType = cowmachine_req:get_resp_header(<<"content-type">>, Context) =/= undefined,
            HasBody = cowmachine_req:resp_body(Context) =/= undefined,
            if
                HasBody andalso HasRespContentType ->
                    {State, Context};
                true ->
                    % Let the error controller handle 4xx and 5xx errors without body
                    controller_call(finish_request, State, Context),
                    throw({stop_request, Code})
            end;
        _ ->
            {State, Context}
    end,
    % Set transfer encoding.
    {State2, Context2} = case cowmachine_req:get_req_header(<<"te">>, Context1) of
        undefined -> {State1, Context1};
        TEHdr -> choose_transfer_encoding(TEHdr, State1, Context1)
    end,
    Context3 = cowmachine_req:set_response_code(Code, Context2),
    controller_call(finish_request, State2, Context3).

-spec respond(Code, Headers, State, Context) -> Result when
	Code :: integer(), 
	Headers :: [{binary(), binary()}], 
	State :: cmstate(), 
	Context :: cowmachine_req:context(),
	Result :: {term(), #cmstate{}, term()}.
respond(Code, Headers, State, Context) ->
    ContextHs = cowmachine_req:set_resp_headers(Headers, Context),
    respond(Code, State, ContextHs).

%% @throws {stop_request, Code, Reason}

error_response(Code, Reason, State, Context) ->
    controller_call(finish_request, State, Context),
    throw({stop_request, Code, Reason}).

% Suppress no_local return error
-dialyzer({nowarn_function, error_response/3}).
error_response(Reason, State, Context) ->
    error_response(500, Reason, State, Context).

decision_test({Test, Rs, Rd}, TestVal, TrueFlow, FalseFlow) ->
    decision_test(Test, TestVal, TrueFlow, FalseFlow, Rs, Rd).


decision_test({error, Reason}, _Test, _TrueFlow, _FalseFlow, State, Context) ->
    error_response(Reason, State, Context);
decision_test({error, Reason0, Reason1}, _Test, _TrueFlow, _FalseFlow, State, Context) ->
    error_response({Reason0, Reason1}, State, Context);
decision_test({halt, Code}, _Test, _TrueFlow, _FalseFlow, State, Context) ->
    respond(Code, State, Context);
decision_test(body, Test, TrueFlow, _FalseFlow, State, Context) ->
    decision_flow(TrueFlow, Test, State, Context);
decision_test(Test, Test, TrueFlow, _FalseFlow, State, Context) ->
    decision_flow(TrueFlow, Test, State, Context);
decision_test(Test, _Test, _TrueFlow, FalseFlow, State, Context) ->
    decision_flow(FalseFlow, Test, State, Context).

decision_test_fn({Test, State, Context}, TestFn, TrueFlow, FalseFlow) ->
    decision_test_fn(Test, TestFn, TrueFlow, FalseFlow, State, Context).

decision_test_fn({error, Reason}, _TestFn, _TrueFlow, _FalseFlow, State, Context) ->
    error_response(Reason, State, Context);
decision_test_fn({error, R0, R1}, _TestFn, _TrueFlow, _FalseFlow, State, Context) ->
    error_response({R0, R1}, State, Context);
decision_test_fn({halt, Code}, _TestFn, _TrueFlow, _FalseFlow, State, Context) ->
    respond(Code, State, Context);
decision_test_fn(Test,TestFn,TrueFlow,FalseFlow, State, Context) ->
    case TestFn(Test) of
        true -> decision_flow(TrueFlow, Test, State, Context);
        false -> decision_flow(FalseFlow, Test, State, Context)
    end.

decision_flow(X, _TestResult, State, Context) when is_atom(X) ->
    d(X, State, Context);
decision_flow(X, _TestResult, State, Context) when is_integer(X), X < 500 ->
    respond(X, State, Context);
decision_flow(X, TestResult, State, Context) when is_integer(X), X >= 500 ->
    error_response(X, TestResult, State, Context).
% decision_flow({ErrCode, Reason}, _TestResult, State, Context) when is_integer(ErrCode) ->
%     error_response(ErrCode, Reason, State, Context).


%% "Service Available"
decision(v3b13, State, Context) ->
    decision_test(controller_call(service_available, State, Context), true, v3b12, 503);

%% "Known method?"
decision(v3b12, State, Context) ->
    {Methods, S1, C1} = controller_call(known_methods, State, Context),
    decision_test(lists:member(cowmachine_req:method(C1), Methods), true, v3b11, 501, S1, C1);

%% "URI too long?"
decision(v3b11, State, Context) ->
    decision_test(controller_call(uri_too_long, State, Context), true, 414, v3b10);

%% "Method allowed?"
decision(v3b10, State, Context) ->
    {Methods, S1, C1} = controller_call(allowed_methods, State, Context),
    case lists:member(cowmachine_req:method(C1), Methods) of
        true ->
            d(v3b9, S1, C1);
        false ->
            CtxAllow = cowmachine_req:set_resp_header(<<"allow">>, [z_convert:to_binary(M) || M <- Methods], C1),
            respond(405, S1, CtxAllow)
    end;

%% "Content-MD5 present?"
decision(v3b9, State, Context) ->
    ContentMD5 = cowmachine_req:get_req_header(<<"content-md5">>, Context),
    decision_test(ContentMD5, undefined, v3b9b, v3b9a, State, Context);

%% "Content-MD5 valid?"
decision(v3b9a, State, Context) ->
    {Md5Valid, S1, C1} = controller_call(validate_content_checksum, State, Context),
    case Md5Valid of
        {error, Reason} ->
            error_response(Reason, S1, C1);
        {halt, Code} ->
            respond(Code, S1, C1);
        not_validated ->
            Checksum = base64:decode(cowmachine_req:get_req_header(<<"content-md5">>, C1)),
            {Body, C2} = cowmachine_req:req_body(C1),
            BodyHash = crypto:hash(md5, Body),
            case BodyHash =:= Checksum of
                true -> d(v3b9b, S1, C2);
                _ ->
                    respond(400, S1, C2)
            end;
        false ->
            respond(400, S1, C1);
        _ ->
            d(v3b9b, S1, C1)
    end;

%% "Malformed?"
decision(v3b9b, State, Context) ->
    decision_test(controller_call(malformed_request, State, Context), true, 400, v3b8);

%% "Authorized?"
decision(v3b8, #cmstate{ options = Options } = State, Context) ->
    Context1 = case maps:get(on_welformed, Options, undefined) of
                    undefined -> Context;
                    Fun when is_function(Fun) -> Fun(Context)
               end,
    {IsAuthorized, S1, C1} = controller_call(is_authorized, State, Context1),
    case IsAuthorized of
        true ->
            d(v3b7, S1, C1);
        false ->
            CtxAuth = cowmachine_req:set_resp_header(<<"www-authenticate">>, <<"z.auth">>, C1),
            respond(401, S1, CtxAuth);
        AuthHead when is_binary(AuthHead) ->
            CtxAuth = cowmachine_req:set_resp_header(<<"www-authenticate">>, AuthHead, C1),
            respond(401, S1, CtxAuth);
        {error, Reason} ->
            error_response(Reason, S1, C1);
        {halt, Code}  ->
            respond(Code, S1, C1)
    end;

%% "Forbidden?"
decision(v3b7, State, Context) ->
    decision_test(controller_call(forbidden, State, Context), true, 403, v3b6_upgrade);

%% "Upgrade?"
decision(v3b6_upgrade, State, Context) ->
    case cowmachine_req:get_req_header(<<"upgrade">>, Context) of
        undefined ->
            decision(v3b6, State, Context);
        UpgradeHdr ->
            case cowmachine_req:get_req_header(<<"connection">>, Context) of
                undefined ->
                    decision(v3b6, State, Context);
                Connection ->
                    case contains_token(<<"upgrade">>, Connection) of
                        true ->
                            {Choosen, S1, C1} = choose_upgrade(UpgradeHdr, State, Context),
                            case Choosen of
                                none ->
                                    decision(v3b6, S1, C1);
                                {_Protocol, UpgradeFunc} ->
                                    {upgrade, UpgradeFunc, S1, C1}
                            end;
                        false ->
                            decision(v3b6, State, Context)
                    end
            end
    end;

%% "Okay Content-* Headers?"
decision(v3b6, State, Context) ->
    decision_test(controller_call(valid_content_headers, State, Context), true, v3b5, 501);

%% "Known Content-Type?"
decision(v3b5, State, Context) ->
    decision_test(controller_call(known_content_type, State, Context), true, v3b4, 415);

%% "Req Entity Too Large?"
decision(v3b4, State, Context) ->
    decision_test(controller_call(valid_entity_length, State, Context), true, v3b3, 413);

%% "OPTIONS?"
decision(v3b3, State, Context) ->
    case cowmachine_req:method(Context) of
        <<"OPTIONS">> ->
            {Hdrs, S1, C1} = controller_call(options, State, Context),
            respond(200, Hdrs, S1, C1);
        _ ->
            d(v3c3, State, Context)
    end;

%% Accept exists?
decision(v3c3, State, Context) ->
    case cowmachine_req:get_req_header(<<"accept">>, Context) of
        undefined ->
            % No accept header, select the first content-type provided
            {ContentTypes, S1, C1} = controller_call(content_types_provided, State, Context),
            MType = cowmachine_util:normalize_content_type( hd(ContentTypes) ),
            C2 = cowmachine_req:set_resp_content_type(MType, C1),
            d(v3d4, S1, C2);
        _ ->
            d(v3c4, State, Context)
    end;

%% Acceptable media type available? (check against Accept header)
decision(v3c4, State, Context) ->
    {ContentTypesProvided, S1, C1} = controller_call(content_types_provided, State, Context),
    AcceptHdr = cowmachine_req:get_req_header(<<"accept">>, C1),
    case cowmachine_util:choose_media_type_provided(ContentTypesProvided, AcceptHdr) of
        none ->
            respond(406, S1, C1);
        MType ->
            C2 = cowmachine_req:set_resp_content_type(MType, C1),
            d(v3d4, S1, C2)
    end;

%% Accept-Language exists?
decision(v3d4, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"accept-language">>, Context), undefined, v3e5, v3d5, State, Context);

%% Acceptable Language available? %% WMACH-46 (do this as proper conneg)
decision(v3d5, State, Context) ->
    decision_test(controller_call(language_available, State, Context), true, v3e5, 406);

%% Accept-Charset exists?
decision(v3e5, State, Context) ->
    case cowmachine_req:get_req_header(<<"accept-charset">>, Context) of
        undefined -> decision_test(choose_charset(<<"*">>, State, Context), none, 406, v3f6);
        _ -> d(v3e6, State, Context)
    end;

%% Acceptable Charset available?
decision(v3e6, State, Context) ->
    decision_test(
        choose_charset(cowmachine_req:get_req_header(<<"accept-charset">>, Context), State, Context),
        none, 406, v3f6);

%% Accept-Encoding exists?
% (also, set content-type header here, now that charset is chosen)
decision(v3f6, State, Context) ->
    {CT1, CT2, CTArgs} = CType = cowmachine_req:resp_content_type(Context),
    CSet = case cowmachine_req:resp_chosen_charset(Context) of
               undefined -> CType;
               CS -> {CT1, CT2, [ {<<"charset">>, CS} | lists:keydelete(<<"charset">>, 1, CTArgs) ]}
           end,
    C1 = cowmachine_req:set_resp_header(<<"content-type">>, cowmachine_util:format_content_type(CSet), Context),
    case cowmachine_req:get_req_header(<<"accept-encoding">>, C1) of
        undefined ->
            decision_test(
                    choose_content_encoding(<<"identity;q=1.0,*;q=0.5">>, State, C1),
                    none, 406, v3g7);
        _ -> d(v3f7, State, C1)
    end;

%% Acceptable encoding available?
decision(v3f7, State, Context) ->
    decision_test(
            choose_content_encoding(cowmachine_req:get_req_header(<<"accept-encoding">>, Context), State, Context),
            none, 406, v3g7);

%% "Resource exists?"
decision(v3g7, State, Context) ->
    % this is the first place after all conneg, so set Vary here
    {Variances, S1, C1} = variances(State, Context),
    VarCtx = case Variances of
        [] -> C1;
        _ -> cowmachine_req:set_resp_header(<<"vary">>, [Variances], C1)
    end,
    decision_test(controller_call(resource_exists, S1, VarCtx), true, v3g8, v3h7);

%% "If-Match exists?"
decision(v3g8, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-match">>, Context), undefined, v3h10, v3g9, State, Context);

%% "If-Match: * exists"
decision(v3g9, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-match">>, Context), <<"*">>, v3h10, v3g11, State, Context);

%% "ETag in If-Match"
decision(v3g11, State, Context) ->
    ETags = cowmachine_util:split_quoted_strings(cowmachine_req:get_req_header(<<"if-match">>, Context)),
    decision_test_fn(controller_call(generate_etag, State, Context),
                     fun(ETag) -> lists:member(ETag, ETags) end,
                     v3h10, 412);

%% "If-Match: * exists"
decision(v3h7, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-match">>, Context), "*", 412, v3i7, State, Context);

%% "If-unmodified-since exists?"
decision(v3h10, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-unmodified-since">>, Context), undefined, v3i12, v3h11, State, Context);

%% "I-UM-S is valid date?"
decision(v3h11, State, Context) ->
    IUMSDate = cowmachine_req:get_req_header(<<"if-unmodified-since">>, Context),
    decision_test(cowmachine_util:convert_request_date(IUMSDate), bad_date, v3i12, v3h12, State, Context);

%% "Last-Modified > I-UM-S?"
decision(v3h12, State, Context) ->
    ReqDate = cowmachine_req:get_req_header(<<"if-unmodified-since">>, Context),
    ReqErlDate = cowmachine_util:convert_request_date(ReqDate),
    {ResErlDate, S1, C1} = controller_call(last_modified, State, Context),
    decision_test(ResErlDate > ReqErlDate, true, 412, v3i12, S1, C1);

%% "Moved permanently? (apply PUT to different URI)"
decision(v3i4, State, Context) ->
    {MovedPermanently, S1, C1} = controller_call(moved_permanently, State, Context),
    case MovedPermanently of
        {true, MovedURI} ->
            LocCtx = cowmachine_req:set_resp_header(<<"location">>, MovedURI, C1),
            respond(301, S1, LocCtx);
        false ->
            d(v3p3, S1, C1);
        {error, Reason} ->
            error_response(Reason, S1, C1);
        {halt, Code} ->
            respond(Code, S1, C1)
    end;

%% PUT?
decision(v3i7, State, Context) ->
    decision_test(cowmachine_req:method(Context), <<"PUT">>, v3i4, v3k7, State, Context);

%% "If-none-match exists?"
decision(v3i12, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-none-match">>, Context), undefined, v3l13, v3i13, State, Context);

%% "If-None-Match: * exists?"
decision(v3i13, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-none-match">>, Context), <<"*">>, v3j18, v3k13, State, Context);

%% GET or HEAD?
decision(v3j18, State, Context) ->
    decision_test(lists:member(cowmachine_req:method(Context),[<<"GET">>, <<"HEAD">>]), true, 304, 412, State, Context);

%% "Moved permanently?"
decision(v3k5, State, Context) ->
    {MovedPermanently, S1, C1} = controller_call(moved_permanently, State, Context),
    case MovedPermanently of
        {true, MovedURI} ->
            LocCtx = cowmachine_req:set_resp_header(<<"location">>, MovedURI, C1),
            respond(301, S1, LocCtx);
        false ->
            d(v3l5, S1, C1);
        {error, Reason} ->
            error_response(Reason, S1, C1);
        {halt, Code} ->
            respond(Code, S1, C1)
    end;

%% "Previously existed?"
decision(v3k7, State, Context) ->
    decision_test(controller_call(previously_existed, State, Context), true, v3k5, v3l7);

%% "Etag in if-none-match?"
decision(v3k13, State, Context) ->
    ETags = cowmachine_util:split_quoted_strings(cowmachine_req:get_req_header(<<"if-none-match">>, Context)),
    decision_test_fn(controller_call(generate_etag, State, Context),
                     %% Membership test is a little counter-intuitive here; if the
                     %% provided ETag is a member, we follow the error case out
                     %% via v3j18.
                     fun(ETag) -> lists:member(ETag, ETags) end,
                     v3j18, v3l13);

%% "Moved temporarily?"
decision(v3l5, State, Context) ->
    {MovedTemporarily, S1, C1} = controller_call(moved_temporarily, State, Context),
    case MovedTemporarily of
    {true, MovedURI} ->
        LocCtx = cowmachine_req:set_resp_header(<<"location">>, MovedURI, C1),
        respond(307, S1, LocCtx);
    false ->
        d(v3m5, S1, C1);
    {error, Reason} ->
        error_response(Reason, S1, C1);
    {halt, Code} ->
        respond(Code, S1, C1)
    end;

%% "POST?"
decision(v3l7, State, Context) ->
    decision_test(cowmachine_req:method(Context), <<"POST">>, v3m7, 404, State, Context);

%% "IMS exists?"
decision(v3l13, State, Context) ->
    decision_test(cowmachine_req:get_req_header(<<"if-modified-since">>, Context), undefined, v3m16, v3l14, State, Context);

%% "IMS is valid date?"
decision(v3l14, State, Context) -> 
    IMSDate = cowmachine_req:get_req_header(<<"if-modified-since">>, Context),
    decision_test(cowmachine_util:convert_request_date(IMSDate), bad_date, v3m16, v3l15, State, Context);

%% "IMS > Now?"
decision(v3l15, State, Context) ->
    NowDateTime = calendar:universal_time(),
    ReqDate = cowmachine_req:get_req_header(<<"if-modified-since">>, Context),
    ReqErlDate = cowmachine_util:convert_request_date(ReqDate),
    decision_test(ReqErlDate > NowDateTime, true, v3m16, v3l17, State, Context);

%% "Last-Modified > IMS?"
decision(v3l17, State, Context) ->
    ReqDate = cowmachine_req:get_req_header(<<"if-modified-since">>, Context),
    ReqErlDate = cowmachine_util:convert_request_date(ReqDate),
    {ResErlDate, S1, C1} = controller_call(last_modified, State, Context),
    decision_test(ResErlDate =:= undefined orelse ResErlDate > ReqErlDate,
                  true, v3m16, 304, S1, C1);

%% "POST?"
decision(v3m5, State, Context) ->
    decision_test(cowmachine_req:method(Context), <<"POST">>, v3n5, 410, State, Context);

%% "Server allows POST to missing resource?"
decision(v3m7, State, Context) ->
    decision_test(controller_call(allow_missing_post, State, Context), true, v3n11, 404);

%% "DELETE?"
decision(v3m16, State, Context) ->
    decision_test(cowmachine_req:method(Context), <<"DELETE">>, v3m20, v3n16, State, Context);

%% DELETE enacted immediately?
%% Also where DELETE is forced.
decision(v3m20, State, Context) ->
    decision_test(accept_process_helper(State, Context), true, v3m20b, 500);
decision(v3m20b, State, Context) ->
    decision_test(controller_call(delete_completed, State, Context), true, v3o20, 202);

%% "Server allows POST to missing resource?"
decision(v3n5, State, Context) ->
    decision_test(controller_call(allow_missing_post, State, Context), true, v3n11, 410);

%% "Redirect?"
decision(v3n11, State, Context) ->
    {PostIsCreate, S1, C1} = controller_call(post_is_create, State, Context),
    {Stage1, SStage1, CtxStage1} = case PostIsCreate of
        true ->
            {CreatePath, S2, C2} = controller_call(create_path, S1, C1),
            case CreatePath of
                NewPath when is_binary(NewPath) ->
                    NewPath1 = case NewPath of
                                    <<$/, Path/binary>> -> Path;
                                    _ -> NewPath
                               end,
                    PathCtx = cowmachine_req:set_disp_path(<<$/, NewPath1/binary>>, C2),
                    {LocS, LocCtx} = case cowmachine_req:get_resp_header(<<"location">>, PathCtx) of
                        undefined ->
                            {BaseUri0, S3, C3} = controller_call(base_uri, S2, PathCtx),
                            BaseUri = case BaseUri0 of
                                            undefined -> cowmachine_req:base_uri(C3);
                                            _ -> BaseUri0
                                        end,
                            Loc = case binary:last(BaseUri) of
                                    $/ -> <<BaseUri/binary, NewPath1/binary>>;
                                    _ -> <<BaseUri/binary, $/, NewPath1/binary>>
                                  end,
                            {S3, cowmachine_req:set_resp_header(<<"location">>, Loc, PathCtx)};
                        _ ->
                            {S2, PathCtx}
                    end,
                    {Res, S4, C4} = accept_process_helper(LocS, LocCtx),
					case Res of
                        {error, _,_} -> error_response(Res, S4, C4);
                        {error, _} -> error_response(Res, S4, C4);
						{halt, Code} -> respond(Code, S4, C4);
                        _ -> {stage1_ok, S4, C4}
                    end;
                undefined ->
                    error_response("post_is_create without create_path", S2, C2);
                _ ->
                    error_response("create_path not a binary string", S2, C2)
            end;
        false ->
            {ProcessPost, S2, C2} = accept_process_helper(S1, C1),
            case ProcessPost of
                {halt, Code} -> respond(Code, S2, C2);
                {error, _} = Err -> error_response(Err, S2, C2);
                {error, _, _} = Err -> error_response(Err, S2, C2);
                _ -> {stage1_ok, S2, C2}
            end
    end,
    case Stage1 of
        stage1_ok ->
            case cowmachine_req:resp_redirect(CtxStage1) of
                true ->
                    case cowmachine_req:get_resp_header(<<"location">>, CtxStage1) of
                        undefined ->
                            cowmachine:log(#{
                                level => error,
                                at => ?AT,
                                code => 500,
                                controller => State#cmstate.controller,
                                text => "Response had set_resp_redirect but no Location header."},
                                Context),
                            respond(500, SStage1, CtxStage1);
                        _ ->
                            respond(303, SStage1, CtxStage1)
                    end;
                false ->
                    d(v3p11, SStage1, CtxStage1)
            end;
        _ ->
            {nop, SStage1, CtxStage1}
    end;

%% "POST?"
decision(v3n16, State, Context) ->
    decision_test(cowmachine_req:method(Context), <<"POST">>, v3n11, v3o16, State, Context);

%% Conflict? -- Only with PUT
decision(v3o14, State, Context) ->
    {IsConflict, S1, C1} = controller_call(is_conflict, State, Context),
    case IsConflict of
        true ->
            respond(409, S1, C1);
        _ ->
            {Res, SHelp, CHelp} = accept_process_helper(S1, C1),
            case Res of
                {halt, Code} -> respond(Code, SHelp, CHelp);
                {error, _, _} -> error_response(Res, SHelp, CHelp);
                {error, _} -> error_response(Res, SHelp, CHelp);
                _ -> d(v3p11, SHelp, CHelp)
            end
    end;

%% "PUT?"
decision(v3o16, State, Context) ->
    decision_test(cowmachine_req:method(Context), <<"PUT">>, v3o14, v3o18, State, Context);

%% Multiple representations?
% (also where body generation for GET and HEAD is done)
decision(v3o18, State, Context) ->
    {ProcessResult, SBody, CBody} = case State#cmstate.is_process_called of
        false ->
            process_helper(undefined, State, Context);
        true ->
            {nop, State, Context}
    end,
    % If a response body was set then also set etag, modified, etc.
    {S2, C2} = case cowmachine_req:has_resp_body(CBody) of
        true ->
            {S1, C1} = etag_etc_helper(SBody, CBody),
            {S1, C1};
        false ->
            {SBody, CBody}
    end,
    case ProcessResult of
        {halt, Code} -> respond(Code, S2, C2);
        {error, _} -> error_response(ProcessResult, S2, C2);
        {error, _,_} -> error_response(ProcessResult, S2, C2);
        _ -> d(v3o18b, S2, C2)
    end;

decision(v3o18b, State, Context) ->
    decision_test(controller_call(multiple_choices, State, Context), true, 300, 200);

%% Response includes an entity?
decision(v3o20, State, Context) ->
    decision_test(cowmachine_req:has_resp_body(Context), true, v3o18, 204, State, Context);

%% Conflict?
decision(v3p3, State, Context) ->
    {IsConflict, S1, C1} = controller_call(is_conflict, State, Context),
    case IsConflict of
        true -> respond(409, S1, C1);
        _ ->
            {Res, SHelp, CHelp} = accept_process_helper(S1, C1),
            case Res of
                {halt, Code} -> respond(Code, SHelp, CHelp);
                {error, _, _} -> error_response(Res, SHelp, CHelp);
                {error, _} -> error_response(Res, SHelp, CHelp);
                _ -> d(v3p11, SHelp, CHelp)
            end
    end;

%% New controller?  (at this point boils down to "has location header")
decision(v3p11, State, Context) ->
    case cowmachine_req:get_resp_header(<<"location">>, Context) of
        undefined -> d(v3o20, State, Context);
        _ -> respond(201, State, Context)
    end.


%% @doc Check if the request content-type is acceptable - if acceptable then also call the
%% controller's process function.

accept_process_helper(State, Context) ->
    case cowmachine_req:has_req_body(Context) orelse should_have_req_body(Context) of
        true ->
            CTParsed = case cowmachine_req:get_req_header(<<"content-type">>, Context) of
                undefined ->
                    {<<"application">>, <<"octet-stream">>, []};
                CTHeader ->
                    cow_http_hd:parse_content_type(CTHeader)
            end,
            RdCT = cowmachine_req:set_metadata(content_type, CTParsed, Context),
            {ContentTypesAccepted, State1, Context1} = controller_call(content_types_accepted, State, RdCT),
            case cowmachine_util:is_media_type_accepted(ContentTypesAccepted, CTParsed) of
                false ->
                    {{halt, 415}, State1, Context1};
                true ->
                    process_helper(CTParsed, State1, Context1)
            end;
        false ->
            process_helper(undefined, State, Context)
    end.

should_have_req_body(Context) ->
    case cowmachine_req:method(Context) of
        <<"POST">> -> true;
        <<"PUT">> -> true;
        <<"PATCH">> -> true;
        _ -> false
    end.

process_helper(_ContentTypeAccepted, #cmstate{ is_process_called = true } = State, Context) ->
    cowmachine:log(#{ level => error, text => "ERROR in process handling, process_helper called twice."}, Context),
    {{error, internal_logic}, State, Context};
process_helper(ContentTypeAccepted, State, Context) ->
    S1 = State#cmstate{ is_process_called = true },
    {Res, S2, C2} = Result = controller_call_process(ContentTypeAccepted, S1, Context),
    case Res of
        {halt, _} -> Result;
        {error, _, _} -> Result;
        {error, _} -> Result;
        true -> Result;
        false -> Result;
        RespBody ->
            C3 = cowmachine_req:set_resp_body(RespBody, C2),
            {body, S2, C3}
    end.

etag_etc_helper(State, Context) ->
    {Etag, SEtag, CEtag0} = controller_call(generate_etag, State, Context),
    CEtag = case Etag of
        undefined -> CEtag0;
        ETag -> cowmachine_req:set_resp_header(<<"etag">>, cowmachine_util:quoted_string(ETag), CEtag0)
    end,

    {LastModified, SLM, CLM0} = controller_call(last_modified, SEtag, CEtag),
    CLM = case LastModified of
        undefined -> CLM0;
        LM -> cowmachine_req:set_resp_header(<<"last-modified">>,
                    z_convert:to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(LM))),
                    CLM0)
    end,

    {Expires, SExp, CExp0} = controller_call(expires, SLM, CLM),
    CExp = case Expires of
        undefined -> CExp0;
        Exp -> cowmachine_req:set_resp_header(<<"expires">>,
                    z_convert:to_binary(httpd_util:rfc1123_date(calendar:universal_time_to_local_time(Exp))),
                    CExp0)
    end,
    CIfRange = check_if_range(Etag, LastModified, CExp),
    {SExp, CIfRange}.


% Only called for 'GET' and 'HEAD' - check if 206 result is allowed
check_if_range(Etag, LastModified, Context) ->
    IsRangeOk = is_if_range_ok(cowmachine_req:get_req_header(<<"if-range">>, Context), Etag, LastModified),
    cowmachine_req:set_range_ok(IsRangeOk, Context).

is_if_range_ok(undefined, _ETag, _LM) ->
    true;
is_if_range_ok(<<"W/\"", _/binary>>, _ETag, _LM) ->
    false;
is_if_range_ok(<<"w/\"", _/binary>>, _ETag, _LM) ->
    false;
is_if_range_ok(<<$", _/binary>>, undefined, _LM) ->
    false;
is_if_range_ok(<<$", _/binary>> = IfETag, ETag, _LM) ->
    ETags = cowmachine_util:split_quoted_strings(IfETag),
    lists:member(ETag, ETags);
is_if_range_ok(Date, _ETag, LM) ->
    ErlDate = cowmachine_util:convert_request_date(Date),
    ErlDate =/= undefined andalso ErlDate >= LM.


choose_content_encoding(AccEncHdr, State, Context) ->
    {EncodingsProvided, Rs1, Rd1} = controller_call(content_encodings_provided, State, Context),
    case cowmachine_util:choose_encoding(EncodingsProvided, AccEncHdr) of
        none ->
            {none, Rs1, Rd1};
        ChosenEnc ->
            RdEnc = case ChosenEnc of
                        <<"identity">> -> Rd1;
                        _ -> cowmachine_req:set_resp_header(<<"content-encoding">>,ChosenEnc, Rd1)
                    end,
            RdEnc1 = cowmachine_req:set_resp_content_encoding(ChosenEnc,RdEnc),
            {ChosenEnc, Rs1, RdEnc1}
    end.

choose_transfer_encoding(AccEncHdr, State, Context) ->
    choose_transfer_encoding(cowmachine_req:version(Context), AccEncHdr, State, Context).

choose_transfer_encoding({1,0}, _AccEncHdr, State, Context) ->
    {State, Context};
choose_transfer_encoding({1,1}, AccEncHdr, State, Context) ->
    {EncodingsProvided, Rs1, Rd1} = controller_call(transfer_encodings_provided, State, Context),
    EncList = [ Enc || {Enc, _Func} <- EncodingsProvided ],
    case cowmachine_util:choose_encoding(EncList, AccEncHdr) of
        none ->
            {Rs1, Rd1};
        <<"identity">> ->
            {Rs1, Rd1};
        ChosenEnc ->
            Enc = {ChosenEnc, _} = lists:keyfind(ChosenEnc, 1, EncodingsProvided),
            RdEnc = cowmachine_req:set_resp_transfer_encoding(Enc, Rd1),
            {Rs1, RdEnc}
    end;
choose_transfer_encoding(_, _AccEncHdr, State, Context) ->
    {State, Context}.


choose_charset(AccCharHdr, State, Context) ->
    {CharsetsProvided, Rs1, Rd1} = controller_call(charsets_provided, State, Context),
    case CharsetsProvided of
        no_charset ->
            {no_charset, Rs1, Rd1};
        CL ->
            CSets = [maybe_old_tuple_value(CSet) || CSet <- CL],
            case cowmachine_util:choose_charset(CSets, AccCharHdr) of
                none ->
                    {none, Rs1, Rd1};
                Charset ->
                    RdCSet = cowmachine_req:set_resp_chosen_charset(Charset, Rd1),
                    {Charset, Rs1, RdCSet}
            end
    end.

maybe_old_tuple_value({A, _}) -> A;
maybe_old_tuple_value(A) -> A.

choose_upgrade(UpgradeHdr, State, Context) ->
    {UpgradesProvided, Rs1, Rd1} = controller_call(upgrades_provided, State, Context),
    Provided1 = [ {z_string:to_lower(Prot), Prot, PFun} || {Prot, PFun} <- UpgradesProvided],
    Requested = [ z_string:to_lower(z_string:trim(Up)) || Up <- binary:split(UpgradeHdr, <<",">>, [global]) ],
    {choose_upgrade1(Requested, Provided1), Rs1, Rd1}.

choose_upgrade1([], _) ->
    none;
choose_upgrade1([Req|Requested], Provided) ->
    case lists:keysearch(Req, 1, Provided) of
        false ->
            choose_upgrade1(Requested, Provided);
        {value, {_, Protocol, UpgradeFun}} ->
            {Protocol, UpgradeFun}
    end.


variances(State, Context) ->
    {ContentTypesProvided, Rs1, Rd1} = controller_call(content_types_provided, State, Context),
    Accept = case length(ContentTypesProvided) of
        1 -> [];
        0 -> [];
        _ -> [<<"accept">>]
    end,
    {EncodingsProvided, Rs2, Rd2} = controller_call(content_encodings_provided, Rs1, Rd1),
    AcceptEncoding = case length(EncodingsProvided) of
        1 -> [];
        0 -> [];
        _ -> [<<"accept-encoding">>]
    end,
    {CharsetsProvided, Rs3, Rd3} = controller_call(charsets_provided, Rs2, Rd2),
    AcceptCharset = case CharsetsProvided of
        no_charset -> 
            [];
        CP ->
            case length(CP) of
                1 -> [];
                0 -> [];
                _ -> [<<"accept-charset">>]
            end
    end,
    {Variances, Rs4, Rd4} = controller_call(variances, Rs3, Rd3),
    {Accept ++ AcceptEncoding ++ AcceptCharset ++ Variances, Rs4, Rd4}.


contains_token(Token, HeaderString) ->
    Tokens = lists:map(fun (T) ->
                            z_string:trim(z_string:to_lower(T))
                       end,
                       binary:split(HeaderString, <<",">>, [global])),
    lists:member(Token, Tokens).