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