219 lines
6.6 KiB
Erlang
219 lines
6.6 KiB
Erlang
|
%% @author Justin Sheehy <justin@basho.com>
|
||
|
%% @author Andy Gross <andy@basho.com>
|
||
|
%% @copyright 2007-2009 Basho Technologies
|
||
|
%%
|
||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
%% you may not use this file except in compliance with the License.
|
||
|
%% You may obtain a copy of the License at
|
||
|
%%
|
||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||
|
%%
|
||
|
%% Unless required by applicable law or agreed to in writing, software
|
||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
%% See the License for the specific language governing permissions and
|
||
|
%% limitations under the License.
|
||
|
|
||
|
-module(webmachine_resource, [R_Mod, R_ModState, R_ModExports, R_Trace]).
|
||
|
-author('Justin Sheehy <justin@basho.com>').
|
||
|
-author('Andy Gross <andy@basho.com>').
|
||
|
-export([wrap/2]).
|
||
|
-export([do/2,log_d/1,stop/0]).
|
||
|
|
||
|
default(ping) ->
|
||
|
no_default;
|
||
|
default(service_available) ->
|
||
|
true;
|
||
|
default(resource_exists) ->
|
||
|
true;
|
||
|
default(auth_required) ->
|
||
|
true;
|
||
|
default(is_authorized) ->
|
||
|
true;
|
||
|
default(forbidden) ->
|
||
|
false;
|
||
|
default(allow_missing_post) ->
|
||
|
false;
|
||
|
default(malformed_request) ->
|
||
|
false;
|
||
|
default(uri_too_long) ->
|
||
|
false;
|
||
|
default(known_content_type) ->
|
||
|
true;
|
||
|
default(valid_content_headers) ->
|
||
|
true;
|
||
|
default(valid_entity_length) ->
|
||
|
true;
|
||
|
default(options) ->
|
||
|
[];
|
||
|
default(allowed_methods) ->
|
||
|
['GET', 'HEAD'];
|
||
|
default(known_methods) ->
|
||
|
['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS'];
|
||
|
default(content_types_provided) ->
|
||
|
[{"text/html", to_html}];
|
||
|
default(content_types_accepted) ->
|
||
|
[];
|
||
|
default(delete_resource) ->
|
||
|
false;
|
||
|
default(delete_completed) ->
|
||
|
true;
|
||
|
default(post_is_create) ->
|
||
|
false;
|
||
|
default(create_path) ->
|
||
|
undefined;
|
||
|
default(process_post) ->
|
||
|
false;
|
||
|
default(language_available) ->
|
||
|
true;
|
||
|
default(charsets_provided) ->
|
||
|
no_charset; % this atom causes charset-negotation to short-circuit
|
||
|
% the default setting is needed for non-charset responses such as image/png
|
||
|
% an example of how one might do actual negotiation
|
||
|
% [{"iso-8859-1", fun(X) -> X end}, {"utf-8", make_utf8}];
|
||
|
default(encodings_provided) ->
|
||
|
[{"identity", fun(X) -> X end}];
|
||
|
% this is handy for auto-gzip of GET-only resources:
|
||
|
% [{"identity", fun(X) -> X end}, {"gzip", fun(X) -> zlib:gzip(X) end}];
|
||
|
default(variances) ->
|
||
|
[];
|
||
|
default(is_conflict) ->
|
||
|
false;
|
||
|
default(multiple_choices) ->
|
||
|
false;
|
||
|
default(previously_existed) ->
|
||
|
false;
|
||
|
default(moved_permanently) ->
|
||
|
false;
|
||
|
default(moved_temporarily) ->
|
||
|
false;
|
||
|
default(last_modified) ->
|
||
|
undefined;
|
||
|
default(expires) ->
|
||
|
undefined;
|
||
|
default(generate_etag) ->
|
||
|
undefined;
|
||
|
default(finish_request) ->
|
||
|
true;
|
||
|
default(_) ->
|
||
|
no_default.
|
||
|
|
||
|
wrap(Mod, Args) ->
|
||
|
case Mod:init(Args) of
|
||
|
{ok, ModState} ->
|
||
|
{ok, webmachine_resource:new(Mod, ModState,
|
||
|
dict:from_list(Mod:module_info(exports)), false)};
|
||
|
{{trace, Dir}, ModState} ->
|
||
|
{ok, File} = open_log_file(Dir, Mod),
|
||
|
log_decision(File, v3b14),
|
||
|
log_call(File, attempt, Mod, init, Args),
|
||
|
log_call(File, result, Mod, init, {{trace, Dir}, ModState}),
|
||
|
{ok, webmachine_resource:new(Mod, ModState,
|
||
|
dict:from_list(Mod:module_info(exports)), File)};
|
||
|
_ ->
|
||
|
{stop, bad_init_arg}
|
||
|
end.
|
||
|
|
||
|
do(Fun, ReqProps) when is_atom(Fun) andalso is_list(ReqProps) ->
|
||
|
Self = proplists:get_value(resource, ReqProps),
|
||
|
Req = proplists:get_value(req, ReqProps),
|
||
|
RD0 = Req:get_reqdata(),
|
||
|
{Reply, RD1, NewModState} = handle_wm_call(Fun, RD0),
|
||
|
case Reply of
|
||
|
{error, Err} -> {Err, Self};
|
||
|
_ ->
|
||
|
Req:set_reqdata(RD1),
|
||
|
{Reply,
|
||
|
webmachine_resource:new(R_Mod, NewModState, R_ModExports, R_Trace)}
|
||
|
end.
|
||
|
|
||
|
handle_wm_call(Fun, ReqData) ->
|
||
|
case default(Fun) of
|
||
|
no_default ->
|
||
|
resource_call(Fun, ReqData);
|
||
|
Default ->
|
||
|
case dict:is_key(Fun, R_ModExports) of % XXX SLOW PROBABLY
|
||
|
true ->
|
||
|
resource_call(Fun, ReqData);
|
||
|
false ->
|
||
|
if is_pid(R_Trace) ->
|
||
|
log_call(R_Trace,
|
||
|
not_exported,
|
||
|
R_Mod, Fun, [ReqData, R_ModState]);
|
||
|
true -> ok
|
||
|
end,
|
||
|
{Default, ReqData, R_ModState}
|
||
|
end
|
||
|
end.
|
||
|
|
||
|
resource_call(F, ReqData) ->
|
||
|
case R_Trace of
|
||
|
false -> nop;
|
||
|
_ -> log_call(R_Trace, attempt, R_Mod, F, [ReqData, R_ModState])
|
||
|
end,
|
||
|
Result = try
|
||
|
apply(R_Mod, F, [ReqData, R_ModState])
|
||
|
catch C:R ->
|
||
|
Reason = {C, R, erlang:get_stacktrace()},
|
||
|
{{error, Reason}, ReqData, R_ModState}
|
||
|
end,
|
||
|
case R_Trace of
|
||
|
false -> nop;
|
||
|
_ -> log_call(R_Trace, result, R_Mod, F, Result)
|
||
|
end,
|
||
|
Result.
|
||
|
|
||
|
log_d(DecisionID) ->
|
||
|
case R_Trace of
|
||
|
false -> nop;
|
||
|
_ -> log_decision(R_Trace, DecisionID)
|
||
|
end.
|
||
|
|
||
|
stop() -> close_log_file(R_Trace).
|
||
|
|
||
|
log_call(File, Type, M, F, Data) ->
|
||
|
io:format(File,
|
||
|
"{~p, ~p, ~p,~n ~p}.~n",
|
||
|
[Type, M, F, escape_trace_data(Data)]).
|
||
|
|
||
|
escape_trace_data(Fun) when is_function(Fun) ->
|
||
|
{'WMTRACE_ESCAPED_FUN',
|
||
|
[erlang:fun_info(Fun, module),
|
||
|
erlang:fun_info(Fun, name),
|
||
|
erlang:fun_info(Fun, arity),
|
||
|
erlang:fun_info(Fun, type)]};
|
||
|
escape_trace_data(Pid) when is_pid(Pid) ->
|
||
|
{'WMTRACE_ESCAPED_PID', pid_to_list(Pid)};
|
||
|
escape_trace_data(List) when is_list(List) ->
|
||
|
escape_trace_list(List, []);
|
||
|
escape_trace_data(Tuple) when is_tuple(Tuple) ->
|
||
|
list_to_tuple(escape_trace_data(tuple_to_list(Tuple)));
|
||
|
escape_trace_data(Other) ->
|
||
|
Other.
|
||
|
|
||
|
escape_trace_list([Head|Tail], Acc) ->
|
||
|
escape_trace_list(Tail, [escape_trace_data(Head)|Acc]);
|
||
|
escape_trace_list([], Acc) ->
|
||
|
%% proper, nil-terminated list
|
||
|
lists:reverse(Acc);
|
||
|
escape_trace_list(Final, Acc) ->
|
||
|
%% non-nil-terminated list, like the dict module uses
|
||
|
lists:reverse(tl(Acc))++[hd(Acc)|escape_trace_data(Final)].
|
||
|
|
||
|
log_decision(File, DecisionID) ->
|
||
|
io:format(File, "{decision, ~p}.~n", [DecisionID]).
|
||
|
|
||
|
open_log_file(Dir, Mod) ->
|
||
|
Now = {_,_,US} = now(),
|
||
|
{{Y,M,D},{H,I,S}} = calendar:now_to_universal_time(Now),
|
||
|
Filename = io_lib:format(
|
||
|
"~s/~p-~4..0B-~2..0B-~2..0B"
|
||
|
"-~2..0B-~2..0B-~2..0B.~6..0B.wmtrace",
|
||
|
[Dir, Mod, Y, M, D, H, I, S, US]),
|
||
|
file:open(Filename, [write]).
|
||
|
|
||
|
close_log_file(File) when is_pid(File) ->
|
||
|
file:close(File);
|
||
|
close_log_file(_) ->
|
||
|
ok.
|