%%%----------------------------------------------------------------------
%%% File : yaws_rss.erl
%%% Created : 15 Dec 2004 by Torbjorn Tornkvist
%%%
%%%
%%%
%%% This means that the default DB won't be used, and
%%% no expiration handling will be done. Only the producing of
%%% XML will thus be done. Also, the whole Opts will be
%%% passed un-interpreted to the other DB module.
If no database exist, a new will be created. %%% The returned database handle is to be used with {@link close/1}. %%% @end %%% open(App, Opts) -> gen_server:call(?SERVER, {open, App, Opts}, infinity). %%% %%% @spec close() -> ok | {error, string()} %%% %%% @doc Close the RSS database. %%% close() -> gen_server:call(?SERVER, {close, ?DB}, infinity). %%% %%% @spec close(DbMod::atom(), DbName::atom()) -> %%% ok | {error, string()} %%% %%% @doc Close the user provided RSS database. %%% A call to; DbMod:close(DbName) will be made. %%% close(DBmod, DBname) -> gen_server:call(?SERVER, {close, DBmod, DBname}, infinity). %%% %%% @spec insert(App::atom(), Tag::atom(), Title::string(), %%% Link::string(), Desc::string()) -> %%% ok | {error, string()} %%% %%% @doc Insert an RSS item into the {App,Tag} RSS feed. %%% An application (App) can maintain several feeds each %%% one refered to with a symbolic name (Tag). %%% Link should be a URL pointing to the item. %%%
In case another database backend is used, the %%% Tag has the format: {DbModule, OpaqueTag} %%% where DbModule is the database backend module %%% to be called, and OpaqueTag the Tag that is %%% used in DbModule:insert(Tag, ...)
%%% @end %%% insert(App, Tag, Title, Link, Desc) -> insert(App, Tag, Title, Link, Desc, ""). %%% %%% @spec insert(App::atom(), Tag::atom(), Title::string(), %%% Link::string(), Desc::string(), %%% Creator::string()) -> %%% ok | {error, string()} %%% %%% @doc Works as {@link insert/5} but takes an extra argument %%% Creator which may contains an identification %%% of who created the item. %%% insert(App, Tag, Title, Link, Desc, Creator) -> GregSecs = calendar:datetime_to_gregorian_seconds({date(),time()}), insert(App, Tag, Title, Link, Desc, Creator, GregSecs). %%% %%% @spec insert(App::atom(), Tag::atom(), Title::string(), %%% Link::string(), Desc::string(), %%% Creator::string(), GregSecs::integer()) -> %%% ok | {error, string()} %%% %%% @doc Works as {@link insert/6} but takes an extra argument %%% GregSecs which is the creation time of the item %%% in Gregorian Seconds. %%% insert(App, Tag, Title, Link, Desc, Creator, GregSecs) -> Args = {App, Tag, Title, Link, Desc, Creator, GregSecs}, gen_server:call(?SERVER, {insert, Args}, infinity). %%% %%% @spec retrieve(App::atom(), Tag::atom()) -> %%% {ok, RSScontent::IoList()} | {error, string()} %%% %%% @type IoList. A deep list of strings and/or binaries. %%% %%% @doc Retrieve the RSScontent (in XML and all...) %%% to be delivered to a RSS client. %%%In case another database backend is used, the %%% Tag has the format: {DbModule, OpaqueTag} %%% where DbModule is the database backend module %%% to be called, and OpaqueTag the Tag that is %%% used in DbModule:retrieve(Tag) which must return %%% a list of tuples: {Title, Link, Desc, Creator, GregSecs}
%%% retrieve(App, Tag) -> gen_server:call(?SERVER, {retrieve, App, Tag}, infinity). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init([]) -> {ok, #s{}}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call({open, App, Opts}, _From, State) -> {NewState, Res} = do_open_dir(State, App, Opts), {reply, Res, NewState}; %% handle_call({close, DB}, _From, State) -> dets:close(DB), {reply, ok, State}; %% handle_call({close, DBMod, DBname}, _From, State) -> catch apply(DBMod, close, [DBname]), {reply, ok, State}; %% handle_call({insert, Args}, _From, State) -> {NewState, Res} = do_insert(State, Args), {reply, Res, NewState}; %% handle_call({retrieve, App, Tag}, _From, State) -> {NewState, Res} = do_retrieve(State, App, Tag), {reply, Res, NewState}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%% %%% Check what database store that should be used. %%% Per default 'dets' is used. %%% do_open_dir(State, App, Opts) -> case get_db_mod(Opts, dets) of dets -> File = get_db_file(Opts), Expire = get_expire(Opts, #s.expire), Max = get_max(Opts, #s.max), Days = get_days(Opts, #s.days), RmExp = get_rm_exp(Opts, #s.rm_exp), case dets:is_dets_file(File) of false -> {State, {error, "not a proper dets file"}}; _ -> case catch dets:open_file(?DB, [{file, File}]) of {ok,DB} = Res -> {State#s{ open_apps = u_insert(App, State#s.open_apps), expire = Expire, days = Days, rm_exp = RmExp, max = Max, counter = init_counter(DB)}, Res}; {error, _Reason} -> {State, {error, "open dets file"}} end end; DBmod -> {State, catch apply(DBmod, open, Opts)} end. get_db_file(Opts) -> Dir = get_db_dir(Opts, "/tmp"), Dir ++ "/" ++ a2l(?DB) ++ ".dets". init_counter(DB) -> case dets:lookup(DB, counter) of [] -> dets:insert(DB, {counter, 0}), 0; [{counter,N}] -> N end. set_counter(DB, N) -> dets:insert(DB, {counter, N}). do_insert(State, {App, {DbMod,Tag}, Title, Link, Desc, Creator, GregSecs}) -> {State, catch apply(DbMod, insert, [App, Tag,Title,Link,Desc,Creator,GregSecs])}; do_insert(State, {App, Tag, Title, Link, Desc, Creator, GregSecs}) -> case lists:member(App, State#s.open_apps) of true -> Counter = if (State#s.max > 0) -> (State#s.counter + 1) rem State#s.max; true -> State#s.counter + 1 end, Item = {Title, Link, Desc, Creator, GregSecs}, Res = dets:insert(?DB, ?ITEM(App, Tag, Counter, Item)), set_counter(?DB, Counter), {State#s{counter = Counter}, Res}; false -> {State, {error, "no open DB"}} end. do_retrieve(State, App, {DbMod,Tag}) -> {State, catch apply(DbMod, retrieve, [App, Tag])}; do_retrieve(State, App, Tag) -> case lists:member(App, State#s.open_apps) of true -> F = fun(?ITEM(Xa, Xt, _Counter, Item), Acc) when Xa == App, Xt == Tag -> [Item|Acc]; (_, Acc) -> Acc end, Items = sort_items(expired(State, dets:foldl(F, [], ?DB))), Xml = to_xml(Items), {State, {ok, Xml}}; false -> {State, {error, "no open DB"}} end. -define(ONE_DAY, 86400). % 24*60*60 seconds -define(X(GregSecs), {Title, Link, Desc, Creator, GregSecs}). %%% Filter away expired items !! expired(State, List) when State#s.expire == days -> Gs = calendar:datetime_to_gregorian_seconds({date(),time()}), Old = Gs - (?ONE_DAY * State#s.days), F = fun(?X(GregSecs), Acc) when GregSecs > Old -> [?X(GregSecs) | Acc]; (_, Acc) -> Acc end, lists:foldl(F, [], List); expired(_State, List) -> List. -undef(X). %%% %%% Sort on creation date !! %%% Item = {Title, Link, Desc, Creator, GregSecs}, %%% sort_items(Is) -> lists:keysort(5,Is). to_xml([{Title, Link, Desc, Creator, GregSecs}|Tail]) -> Date = w3cdtf(GregSecs), [["