1%% @author Justin Sheehy <justin@basho.com> 2%% @author Andy Gross <andy@basho.com> 3%% @copyright 2007-2014 Basho Technologies 4%% 5%% Licensed under the Apache License, Version 2.0 (the "License"); 6%% you may not use this file except in compliance with the License. 7%% You may obtain a copy of the License at 8%% 9%% http://www.apache.org/licenses/LICENSE-2.0 10%% 11%% Unless required by applicable law or agreed to in writing, software 12%% distributed under the License is distributed on an "AS IS" BASIS, 13%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14%% See the License for the specific language governing permissions and 15%% limitations under the License. 16 17-module(webmachine_resource). 18-author('Justin Sheehy <justin@basho.com>'). 19-author('Andy Gross <andy@basho.com>'). 20-export([new/3, wrap/2]). 21-export([do/3,log_d/2,stop/1]). 22 23-include("wm_resource.hrl"). 24-include("wm_reqdata.hrl"). 25-include("wm_reqstate.hrl"). 26 27-type t() :: #wm_resource{}. 28-export_type([t/0]). 29 30-define(CALLBACK_ARITY, 2). 31 32new(R_Mod, R_ModState, R_Trace) -> 33 case erlang:module_loaded(R_Mod) of 34 false -> code:ensure_loaded(R_Mod); 35 true -> ok 36 end, 37 #wm_resource{ 38 module = R_Mod, 39 modstate = R_ModState, 40 trace = R_Trace 41 }. 42 43default(service_available) -> 44 true; 45default(resource_exists) -> 46 true; 47default(is_authorized) -> 48 true; 49default(forbidden) -> 50 false; 51default(allow_missing_post) -> 52 false; 53default(malformed_request) -> 54 false; 55default(uri_too_long) -> 56 false; 57default(known_content_type) -> 58 true; 59default(valid_content_headers) -> 60 true; 61default(valid_entity_length) -> 62 true; 63default(options) -> 64 []; 65default(allowed_methods) -> 66 ['GET', 'HEAD']; 67default(known_methods) -> 68 ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS']; 69default(content_types_provided) -> 70 [{"text/html", to_html}]; 71default(content_types_accepted) -> 72 []; 73default(delete_resource) -> 74 false; 75default(delete_completed) -> 76 true; 77default(post_is_create) -> 78 false; 79default(create_path) -> 80 undefined; 81default(base_uri) -> 82 undefined; 83default(process_post) -> 84 false; 85default(language_available) -> 86 true; 87default(charsets_provided) -> 88 no_charset; % this atom causes charset-negotation to short-circuit 89 % the default setting is needed for non-charset responses such as image/png 90 % an example of how one might do actual negotiation 91 % [{"iso-8859-1", fun(X) -> X end}, {"utf-8", make_utf8}]; 92default(encodings_provided) -> 93 [{"identity", fun(X) -> X end}]; 94 % this is handy for auto-gzip of GET-only resources: 95 % [{"identity", fun(X) -> X end}, {"gzip", fun(X) -> zlib:gzip(X) end}]; 96default(variances) -> 97 []; 98default(is_conflict) -> 99 false; 100default(multiple_choices) -> 101 false; 102default(previously_existed) -> 103 false; 104default(moved_permanently) -> 105 false; 106default(moved_temporarily) -> 107 false; 108default(last_modified) -> 109 undefined; 110default(expires) -> 111 undefined; 112default(generate_etag) -> 113 undefined; 114default(finish_request) -> 115 true; 116default(validate_content_checksum) -> 117 not_validated; 118default(_) -> 119 no_default. 120 121-spec wrap(module(), [any()]) -> 122 {ok, t()} | {stop, bad_init_arg}. 123wrap(Mod, Args) -> 124 case Mod:init(Args) of 125 {ok, ModState} -> 126 {ok, webmachine_resource:new(Mod, ModState, false)}; 127 {{trace, Dir}, ModState} -> 128 {ok, File} = open_log_file(Dir, Mod), 129 log_decision(File, v3b14), 130 log_call(File, attempt, Mod, init, Args), 131 log_call(File, result, Mod, init, {{trace, Dir}, ModState}), 132 {ok, webmachine_resource:new(Mod, ModState, File)}; 133 _ -> 134 {stop, bad_init_arg} 135 end. 136 137do(#wm_resource{}=Res, Fun, ReqProps) -> 138 do(Fun, ReqProps, Res); 139do(Fun, ReqProps, 140 #wm_resource{ 141 module=R_Mod, 142 trace=R_Trace 143 }=Req) 144 when is_atom(Fun) andalso is_list(ReqProps) -> 145 case lists:keyfind(reqstate, 1, ReqProps) of 146 false -> RState0 = undefined; 147 {reqstate, RState0} -> ok 148 end, 149 put(tmp_reqstate, empty), 150 {Reply, ReqData, NewModState} = handle_wm_call(Fun, 151 (RState0#wm_reqstate.reqdata)#wm_reqdata{wm_state=RState0}, 152 Req), 153 ReqState = case get(tmp_reqstate) of 154 empty -> RState0; 155 X -> X 156 end, 157 %% Do not need the embedded state anymore 158 TrimData = ReqData#wm_reqdata{wm_state=undefined}, 159 {Reply, 160 webmachine_resource:new(R_Mod, NewModState, R_Trace), 161 ReqState#wm_reqstate{reqdata=TrimData}}. 162 163handle_wm_call(Fun, ReqData, 164 #wm_resource{ 165 module=R_Mod, 166 modstate=R_ModState, 167 trace=R_Trace 168 }=Req) -> 169 case default(Fun) of 170 no_default -> 171 resource_call(Fun, ReqData, Req); 172 Default -> 173 case erlang:function_exported(R_Mod, Fun, ?CALLBACK_ARITY) of 174 true -> 175 resource_call(Fun, ReqData, Req); 176 false -> 177 if is_pid(R_Trace) -> 178 log_call(R_Trace, 179 not_exported, 180 R_Mod, Fun, [ReqData, R_ModState]); 181 true -> ok 182 end, 183 {Default, ReqData, R_ModState} 184 end 185 end. 186 187trim_trace([{M,F,[RD = #wm_reqdata{},S],_}|STRest]) -> 188 TrimState = (RD#wm_reqdata.wm_state)#wm_reqstate{reqdata='REQDATA'}, 189 TrimRD = RD#wm_reqdata{wm_state=TrimState}, 190 [{M,F,[TrimRD,S]}|STRest]; 191trim_trace(X) -> X. 192 193resource_call(F, ReqData, 194 #wm_resource{ 195 module=R_Mod, 196 modstate=R_ModState, 197 trace=R_Trace 198 }) -> 199 case R_Trace of 200 false -> nop; 201 _ -> log_call(R_Trace, attempt, R_Mod, F, [ReqData, R_ModState]) 202 end, 203 Result = try 204 %% Note: the argument list must match the definition of CALLBACK_ARITY 205 apply(R_Mod, F, [ReqData, R_ModState]) 206 catch C:R -> 207 Reason = {C, R, trim_trace(erlang:get_stacktrace())}, 208 {{error, Reason}, ReqData, R_ModState} 209 end, 210 case R_Trace of 211 false -> nop; 212 _ -> log_call(R_Trace, result, R_Mod, F, Result) 213 end, 214 Result. 215 216log_d(#wm_resource{}=Res, DecisionID) -> 217 log_d(DecisionID, Res); 218log_d(DecisionID, 219 #wm_resource{ 220 trace=R_Trace 221 }) -> 222 case R_Trace of 223 false -> nop; 224 _ -> log_decision(R_Trace, DecisionID) 225 end. 226 227stop(#wm_resource{trace=R_Trace}) -> close_log_file(R_Trace). 228 229log_call(File, Type, M, F, Data) -> 230 io:format(File, 231 "{~p, ~p, ~p,~n ~p}.~n", 232 [Type, M, F, escape_trace_data(Data)]). 233 234escape_trace_data(Fun) when is_function(Fun) -> 235 {'WMTRACE_ESCAPED_FUN', 236 [erlang:fun_info(Fun, module), 237 erlang:fun_info(Fun, name), 238 erlang:fun_info(Fun, arity), 239 erlang:fun_info(Fun, type)]}; 240escape_trace_data(Pid) when is_pid(Pid) -> 241 {'WMTRACE_ESCAPED_PID', pid_to_list(Pid)}; 242escape_trace_data(Port) when is_port(Port) -> 243 {'WMTRACE_ESCAPED_PORT', erlang:port_to_list(Port)}; 244escape_trace_data(List) when is_list(List) -> 245 escape_trace_list(List, []); 246escape_trace_data(R=#wm_reqstate{}) -> 247 list_to_tuple( 248 escape_trace_data( 249 tuple_to_list(R#wm_reqstate{reqdata='WMTRACE_NESTED_REQDATA'}))); 250escape_trace_data(Tuple) when is_tuple(Tuple) -> 251 list_to_tuple(escape_trace_data(tuple_to_list(Tuple))); 252escape_trace_data(Other) -> 253 Other. 254 255escape_trace_list([Head|Tail], Acc) -> 256 escape_trace_list(Tail, [escape_trace_data(Head)|Acc]); 257escape_trace_list([], Acc) -> 258 %% proper, nil-terminated list 259 lists:reverse(Acc); 260escape_trace_list(Final, Acc) -> 261 %% non-nil-terminated list, like the dict module uses 262 lists:reverse(tl(Acc))++[hd(Acc)|escape_trace_data(Final)]. 263 264log_decision(File, DecisionID) -> 265 io:format(File, "{decision, ~p}.~n", [DecisionID]). 266 267open_log_file(Dir, Mod) -> 268 Now = {_,_,US} = os:timestamp(), 269 {{Y,M,D},{H,I,S}} = calendar:now_to_universal_time(Now), 270 Filename = io_lib:format( 271 "~s/~p-~4..0B-~2..0B-~2..0B" 272 "-~2..0B-~2..0B-~2..0B.~6..0B.wmtrace", 273 [Dir, Mod, Y, M, D, H, I, S, US]), 274 file:open(Filename, [write]). 275 276close_log_file(File) when is_pid(File) -> 277 file:close(File); 278close_log_file(_) -> 279 ok. 280