Permalink
Browse files

Merge remote branch 'davide/master'

  • Loading branch information...
2 parents 6bbb761 + a814b78 commit 4db7537588c6b7cdfb2d51dfb9be26ca2ca84bca @hntrmrrs hntrmrrs committed Nov 13, 2009
View
@@ -61,8 +61,18 @@
-export([server_request_foldl/4]).
%% Utility methods
--export([parse_qs/1, parse_post/1, parse_post/2, urlencode/1, quote/1,
- normalize_header/1, unquote_path/1, path_components/3, urlsplit/1]).
+-export([parse_qs/1, parse_post/1, urlencode/1, quote/1, normalize_header/1,
+ unquote_path/1, path_components/3, urlsplit/1]).
+
+%% Stream methods
+-export([
+ stream_process_init/2,
+ stream_process_init/3,
+ stream_process_deliver/2,
+ stream_process_deliver_chunk/2,
+ stream_process_deliver_final_chunk/2,
+ stream_process_end/1
+ ]).
%%====================================================================
%% API
@@ -454,43 +464,26 @@ parse_qs(ToParse) ->
%% @end
%%--------------------------------------------------------------------
parse_post(ToParse) ->
- parse_data(ToParse, "ISO-8859-1").
-
-%%--------------------------------------------------------------------
-%% @spec parse_post(string()|binary(), InEncoding) -> [proplist()]
-%%
-%% @doc Parse application/x-www-form-urlencoded data.
-%% Calls parse_data to do the job.
-%% @end
-%%--------------------------------------------------------------------
-parse_post(ToParse, InEncoding) ->
- parse_data(ToParse, InEncoding).
+ parse_data(ToParse).
%%--------------------------------------------------------------------
%% @spec parse_data(string()|binary()) -> [proplist()]
%%
%% @doc Parse a query string or application/x-www-form-urlencoded data.
%% @end
%%--------------------------------------------------------------------
-parse_data(undefined, _InEncoding) ->
+parse_data(undefined) ->
[];
-parse_data(Data, InEncoding) ->
- UTFData = unicode_data(Data, string:to_lower(InEncoding)),
- kv_data(UTFData, []).
-
-unicode_data(Data, "iso-8859-1") ->
- unicode:characters_to_list(Data, latin1);
-unicode_data(Data, "utf8") ->
- unicode:characters_to_list(Data, utf8);
-unicode_data(Data, "utf-8") ->
- unicode:characters_to_list(Data, utf8).
-%% TODO: add support for more charsets
-
-kv_data([], Acc) ->
+parse_data(Binary) when is_binary(Binary) ->
+ parse_data(binary_to_list(Binary), []);
+parse_data(String) ->
+ parse_data(String, []).
+
+parse_data([], Acc) ->
lists:reverse(Acc);
-kv_data(String, Acc) ->
+parse_data(String, Acc) ->
{{Key, Val}, Rest} = parse_kv(String),
- kv_data(Rest, [{Key, Val} | Acc]).
+ parse_data(Rest, [{Key, Val} | Acc]).
%%--------------------------------------------------------------------
@@ -864,3 +857,62 @@ urlsplit_query("#" ++ Rest, Acc) ->
{lists:reverse(Acc), Rest};
urlsplit_query([C | Rest], Acc) ->
urlsplit_query(Rest, [C | Acc]).
+
+%%--------------------------------------------------------------------
+%% Stream methods
+%%--------------------------------------------------------------------
+%% chunked response
+stream_process_init(Ctx, chunked) when ?IS_EWGI_CONTEXT(Ctx) ->
+ {StatusCode, _} = ewgi_api:response_status(Ctx),
+ Headers = ewgi_api:response_headers(Ctx),
+ ChunkedHeader = {"Transfer-Encoding", "chunked"},
+ wait_for_socket(StatusCode, [ChunkedHeader|Headers], chunked);
+
+%% non chunked response
+stream_process_init(Ctx, CL) when ?IS_EWGI_CONTEXT(Ctx), is_integer(CL) ->
+ {StatusCode, _} = ewgi_api:response_status(Ctx),
+ Headers = ewgi_api:response_headers(Ctx),
+ CLHeader = {"Content-Length", integer_to_list(CL)},
+ wait_for_socket(StatusCode, [CLHeader|Headers], non_chunked).
+
+%% This API is for processes that don't have access the original ewgi_context()
+stream_process_init(StatusCode, Headers, chunked) ->
+ ChunkedHeader = {"Transfer-Encoding", "chunked"},
+ wait_for_socket(StatusCode, [ChunkedHeader|Headers], chunked);
+stream_process_init(StatusCode, Headers, CL) when is_integer(CL) ->
+ CLHeader = {"Content-Length", integer_to_list(CL)},
+ wait_for_socket(StatusCode, [CLHeader|Headers], non_chunked).
+
+-define(STREAM_INIT_TIMEOUT, 5000).
+
+wait_for_socket(StatusCode, Headers, TransferEncoding) ->
+ receive
+ {push_stream_init, ServerModule, ServerPid, Socket} ->
+ ServerPid ! {push_stream_init, self(), StatusCode, Headers, TransferEncoding},
+ Connection = {ServerModule, ServerPid, Socket},
+ %% The server should report back on whether we should send data.
+ %% Sometimes (Method='HEAD') only the headers are sent.
+ receive
+ {ok, ServerPid} ->
+ {ok, Connection};
+ {discard, ServerPid} ->
+ stream_process_end(Connection),
+ discard
+ end
+ after ?STREAM_INIT_TIMEOUT ->
+ error_logger:error_msg(?MODULE_STRING ++": Timeout while trying to init stream process!~n"),
+ discard
+ end.
+
+stream_process_deliver({ServerModule, _ServerPid, Socket}, IoList) ->
+ ServerModule:stream_process_deliver(Socket, IoList).
+
+stream_process_deliver_chunk({ServerModule, _ServerPid, Socket}, IoList) ->
+ ServerModule:stream_process_deliver_chunk(Socket, IoList).
+
+stream_process_deliver_final_chunk({ServerModule, _ServerPid, Socket}, IoList) ->
+ ServerModule:stream_process_deliver_final_chunk(Socket, IoList).
+
+stream_process_end({ServerModule, ServerPid, Socket}) ->
+ ServerModule:stream_process_end(Socket, ServerPid).
+
@@ -51,26 +51,52 @@ post_parse_middleware(MaxLength, App, ErrApp)
parse_ct(L) when is_list(L) ->
case string:tokens(L, ";") of
[CT|Vars] ->
- Vars1 = [string:tokens(VarStr, "=") || VarStr <- Vars],
- Vars2 = [{string:strip(Name), Value} || [Name, Value] <- Vars1],
- %% http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html ->
- %% When no explicit charset parameter is provided by the sender,
- %% media subtypes of the "text" type are defined to have a default
- %% charset value of "ISO-8859-1" when received via HTTP.
- Charset = proplists:get_value("charset", Vars2, "ISO-8859-1"),
- {CT, Charset};
+ Vars1 = [string:tokens(VarStr, "=") || VarStr <- Vars],
+ Vars2 = [{string:strip(Name), Value} || [Name, Value] <- Vars1],
+ {CT, Vars2};
_ ->
undefined
end.
-parse_post(Ctx, App, ErrApp, {"application/x-www-form-urlencoded", Charset}, Max) ->
+parse_post(Ctx, App, ErrApp, {"application/x-www-form-urlencoded", Vars}, Max) ->
case ewgi_api:content_length(Ctx) of
L when is_integer(L), L > Max ->
- %% shouldn't we set an error message here?
+ %% shouldn't we set an error message here?
ErrApp(Ctx);
L when is_integer(L), L > 0 ->
Input = read_input_string(Ctx, L),
- Vals = ewgi_api:parse_post(Input, Charset),
+ %% http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
+ %% When no explicit charset parameter is provided by the sender,
+ %% media subtypes of the "text" type are defined to have a default
+ %% charset value of "ISO-8859-1" when received via HTTP.
+ case proplists:get_value("charset", Vars) of
+ undefined -> InCharset = "iso-8859-1"
+ ;Charset -> InCharset = string:to_lower(Charset)
+ end,
+ UnicodeInput = to_unicode(Input, InCharset),
+ Vals = ewgi_api:parse_post(UnicodeInput),
+ Ctx1 = ewgi_api:remote_user_data(Vals, Ctx),
+ App(Ctx1);
+ _ ->
+ ErrApp(Ctx)
+ end;
+parse_post(Ctx, App, ErrApp, {"application/json", _Vars}, Max) ->
+ case ewgi_api:content_length(Ctx) of
+ L when is_integer(L), L > Max ->
+ %% shouldn't we set an error message here?
+ ErrApp(Ctx);
+ L when is_integer(L), L > 0 ->
+ Input = read_input_string(Ctx, L),
+ %% http://www.ietf.org/rfc/rfc4627.txt
+ %% JSON text SHALL be encoded in Unicode.
+ %% The default encoding is UTF-8.
+ case unicode:bom_to_encoding(Input) of
+ {latin1,0} -> InEncoding = utf8
+ ;{InEncoding, _Length} -> ok
+ end,
+ UnicodeInput = unicode:characters_to_list(Input, InEncoding),
+ {Json, [], _} = ktj_decode:decode(UnicodeInput),
+ Vals = [{"json", Json}],
Ctx1 = ewgi_api:remote_user_data(Vals, Ctx),
App(Ctx1);
_ ->
@@ -91,6 +117,15 @@ read_input_string_cb(Acc) ->
read_input_string_cb([B|Acc])
end.
+%% Transforms the data from the given charset to unicode
+%% Todo: add support for other charset as needed.
+to_unicode(Data, "iso-8859-1") ->
+ unicode:characters_to_list(Data, latin1);
+to_unicode(Data, "utf8") ->
+ unicode:characters_to_list(Data, utf8);
+to_unicode(Data, "utf-8") ->
+ unicode:characters_to_list(Data, utf8).
+
%%
%% example functions on how to use the post handling middleware
%%
@@ -113,7 +148,7 @@ display_form_data({ewgi_context, Request, _Response}=Ctx) ->
Body1 ->
io_lib:format("~p", [Body1])
end,
- ResponseHeaders = [{"Content-type", "text/plain"}],
+ ResponseHeaders = [{"Content-type", "text/html; charset=utf8"}],
Response = {ewgi_response,
{200, "OK"},
ResponseHeaders,
@@ -0,0 +1,126 @@
+%% @author Davide Marquês <nesrait@gmail.com>
+%% @copyright 2009 Davide Marquês <nesrait@gmail.com>
+%%
+%% @doc ewgi Push streams middleware.
+%% The difference for regular [pull-]streams is that those are synchronously polled
+%% by the ewgi server for more data. With pull streams the ewgi server is notified
+%% when there is more data available.
+%% @end
+%%
+%% Licensed under the MIT license:
+%% http://www.opensource.org/licenses/mit-license.php
+%%
+-module(ewgi_push_stream).
+-author('Davide Marquês <nesrait@gmail.com>').
+
+-define(STREAM_INIT_TIMEOUT, 5000).
+
+-define(EX_STREAM_DATA, ["ini\r\n", "mini\r\n", "mini\r\n", "mo\r\n"]).
+
+%% Ewgi Application API
+-export([run/2]).
+
+%% Usage examples
+-export([
+ chunked_stream_example/1,
+ non_chunked_stream_example/1,
+ ewgi_free_stream_example/1
+ ]).
+
+%% The body field is used to specify which process will be producing data.
+%% The various headers and data are all send asynchronously to the
+%% ewgi gateway when available.
+%% The given timeout defines how long to want before issuing a 504 response.
+run(Ctx, [StreamPid]) when is_pid(StreamPid) ->
+ PS = {push_stream, StreamPid, ?STREAM_INIT_TIMEOUT},
+ ewgi_api:response_message_body(PS, Ctx);
+
+run(Ctx, [StreamPid, Timeout]) when is_pid(StreamPid) ->
+ PS = {push_stream, StreamPid, Timeout},
+ ewgi_api:response_message_body(PS, Ctx).
+
+%%--------------------------------------------------------------------
+%% Chunked stream example
+%%--------------------------------------------------------------------
+%% N.B.: there's no point in setting headers in the ewgi_context
+%% passed to ewgi_push_stream:run/2. Those headers will be ignore!
+chunked_stream_example(Ctx) ->
+ StreamPid = spawn(fun() -> chunked_stream(Ctx) end),
+ ?MODULE:run(Ctx, [StreamPid]).
+
+%% The ewgi_context that is passed to ewgi_api:stream_process_init/2 is
+%% the one from where the status code and headers are read from.
+%% The default is a chunked response (see below for non-chunked responses).
+chunked_stream(Ctx0) ->
+ Status = {200, "OK"},
+ H = ewgi_api:response_headers(Ctx0),
+ CTHeader = {"Content-type", "text/plain"},
+ Ctx = ewgi_api:response_headers([CTHeader|H],
+ ewgi_api:response_status(Status, Ctx0)),
+
+ CSEnd = "chunked_stream_end",
+ case ewgi_api:stream_process_init(Ctx, chunked) of
+ {ok, Connection} ->
+ lists:foreach(fun(Word) ->
+ ewgi_api:stream_process_deliver_chunk(Connection, Word),
+ timer:sleep(1000)
+ end, ?EX_STREAM_DATA),
+ ewgi_api:stream_process_deliver_final_chunk(Connection, CSEnd),
+ ewgi_api:stream_process_end(Connection);
+ _ -> ok
+ end.
+
+%%--------------------------------------------------------------------
+%% Non-Chunked stream example
+%%--------------------------------------------------------------------
+%% N.B.: there's no point in setting headers in the ewgi_context
+%% passed to ewgi_push_stream:run/2. Those headers will be ignore!
+non_chunked_stream_example(Ctx) ->
+ StreamPid = spawn(fun() -> non_chunked_stream(Ctx) end),
+ ?MODULE:run(Ctx, [StreamPid]).
+
+%% The ewgi_context that is passed to ewgi_api:stream_process_init/2 is
+%% the one from where the status code and headers are read from.
+%% By passing a content-length we specify that this woun't be a chunked response.
+non_chunked_stream(Ctx0) ->
+ Status = {200, "OK"},
+ H = ewgi_api:response_headers(Ctx0),
+ CTHeader = {"Content-type", "text/plain"},
+ Ctx = ewgi_api:response_headers([CTHeader|H],
+ ewgi_api:response_status(Status, Ctx0)),
+
+ NCSEnd = "non_chunked_stream_end",
+ ContentLength = iolist_size(?EX_STREAM_DATA) + iolist_size(NCSEnd),
+ case ewgi_api:stream_process_init(Ctx, ContentLength) of
+ {ok, Connection} ->
+ lists:foreach(fun(Word) ->
+ ewgi_api:stream_process_deliver(Connection, Word),
+ timer:sleep(1000)
+ end, ?EX_STREAM_DATA),
+ ewgi_api:stream_process_deliver(Connection, NCSEnd),
+ ewgi_api:stream_process_end(Connection);
+ _ -> ok
+ end.
+
+%%--------------------------------------------------------------------
+%% Ewgi-free stream
+%% Just showing off that any process can serve as a push stream
+%%--------------------------------------------------------------------
+ewgi_free_stream_example(Ctx) ->
+ StreamPid = spawn(fun ewgi_free_stream/0),
+ ?MODULE:run(Ctx, [StreamPid]).
+
+ewgi_free_stream() ->
+ EFSEnd = "ewgi_free_stream_end",
+ StatusCode = 200,
+ Headers = [{"Content-type", "text/plain"}],
+ case ewgi_api:stream_process_init(StatusCode, Headers, chunked) of
+ {ok, Connection} ->
+ lists:foreach(fun(Word) ->
+ ewgi_api:stream_process_deliver_chunk(Connection, Word),
+ timer:sleep(1000)
+ end, ?EX_STREAM_DATA),
+ ewgi_api:stream_process_deliver_final_chunk(Connection, EFSEnd),
+ ewgi_api:stream_process_end(Connection);
+ _ -> ok
+ end.
Oops, something went wrong.

0 comments on commit 4db7537

Please sign in to comment.