Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 421 lines (368 sloc) 14.274 kB
6e45786 Added RSS store/interface.
Tobbe Tornquist authored
1 %%%----------------------------------------------------------------------
2 %%% File : yaws_rss.erl
3 %%% Created : 15 Dec 2004 by Torbjorn Tornkvist <tobbe@tornkvist.org>
4 %%%
5 %%% @doc A Yaws RSS feed interface.
6 %%%
7 %%% @author Torbjörn Törnkvist <tobbe@tornkvist.org>
8 %%% @end
9 %%%
10 %%% $Id$
11 %%%----------------------------------------------------------------------
12 -module(yaws_rss).
13
14 -behaviour(gen_server).
15
16 %% External exports
17 -export([start/0, start_link/0, open/0, open/1, close/0, close/2,
18 insert/4, insert/5, insert/6, retrieve/1]).
19
20 -export([t_setup/0, t_exp/0, t_xopen/0]).
21
22 %% gen_server callbacks
23 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
24
25 -record(s, {
26 expire = false, % false | days
27 rm_exp = false, % remove expired items
28 max=infinite, % maximum number of elements in DB
29 days=7, % maximum number of days in DB
30 counter=0}). % item counter
31
32 -define(SERVER, ?MODULE).
33 -define(DB, ?MODULE).
34 -define(DB_FNAME, "yaws_rss.dets").
35 -define(ITEM(Tag, Counter, Item), {{Tag, Counter}, Item}).
36
37 %%%----------------------------------------------------------------------
38 %%% API
39 %%%----------------------------------------------------------------------
40 start_link() ->
41 gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
42
43 start() ->
44 gen_server:start({local, ?SERVER}, ?MODULE, [], []).
45
46 %%%
47 %%% @spec open(Dir::string()) -> {ok, DB::db()} | {error, string()}
48 %%%
49 %%% @type db(). An opaque handle leading to an RSS database.
50 %%%
51 %%% @doc See {@open/2}
52 %%%
53 open() ->
54 open([]).
55
56 %%%
57 %%% @spec open(Dir::string(), Opts::list()) ->
58 %%% {ok, DB::db()} | {error, string()}
59 %%%
60 %%% @doc Open a RSS database, located at <em>Dir</em>.
61 %%% Per default <em>dets</em> is used as database,
62 %%% but by using the <em>db_mod</em> option it is
63 %%% possible to use your own database.</br>
64 %%% These are the options:
65 %%% <p><dl>
66 %%%
67 %%% <dt>{db_mod, Module}</dt>
68 %%% <dd>If specified, the following functions will be
69 %%% called:<ul>
70 %%% <li>Module:open(Opts)</li>
71 %%% <li>Module:insert(Tag,Title,Link,Desc,Creator,GregSec)</li>
72 %%% <li>Module:retrieve(Tag) -&gt; {Title, Link, Desc, Creator, GregSecs}</li>
73 %%% <li>Module:close(DbName)</li></ul>
74 %%% This means that the default DB won't be used, and
75 %%% no expiration handling will be done. Only the producing of
76 %%% XML will thus be done. Also, the whole <em>Opts</em> will be
77 %%% passed un-interpreted to the other DB module.</dd>
78 %%%
79 %%% <dt>{db_file, File}</dt>
80 %%% <dd>Specifies the full path pointing to the dets-file to be opened.</dd>
81 %%%
82 %%% <dt>{expire, Expire}</dt>
83 %%% <dd>Specifies what method to use to expire items. Possible values
84 %%% are: <em>false</em>, <em>days</em>, meaning
85 %%% never expire, expire after a number of days.
86 %%% Default is to never expire items.</dd>
87 %%%
88 %%% <dt>{days, Number}</dt>
89 %%% <dd>Specifies the number of days befor an item is expired.
90 %%% Default is 7 days.</dd>
91 %%%
92 %%% <dt>{rm_exp, Bool}</dt>
93 %%% <dd>Specifies if expired items should be removed from
94 %%% the database. Default is to not remove any items.</dd>
95 %%%
96 %%% <dt>{max, Number}</dt>
97 %%% <dd>Specifies the maximum number of items that should
98 %%% be stored in the database. The default in <em>infinite</em></dd>
99 %%% </dl></p>
100 %%% <p>If no database exist, a new will be created.
101 %%% The returned database handle is to be used with {@link close/1}.
102 %%% @end
103 %%%
104 open(Opts) ->
105 gen_server:call(?SERVER, {open, Opts}, infinity).
106
107 %%%
108 %%% @spec close() -> ok | {error, string()}
109 %%%
110 %%% @doc Close the RSS database.
111 %%%
112 close() ->
113 gen_server:call(?SERVER, {close, ?DB}, infinity).
114
115 %%%
116 %%% @spec close(DbMod::atom(), DbName::atom()) ->
117 %%% ok | {error, string()}
118 %%%
119 %%% @doc Close the user provided RSS database.
120 %%% A call to; <em>DbMod:close(DbName)</em> will be made.
121 %%%
122 close(DBmod, DBname) ->
123 gen_server:call(?SERVER, {close, DBmod, DBname}, infinity).
124
125 %%%
126 %%% @spec insert(Tag::atom(), Title::string(),
127 %%% Link::string(), Desc::string()) ->
128 %%% ok | {error, string()}
129 %%%
130 %%% @doc Insert an RSS item into the <em>Tag</em> RSS feed.
131 %%% <em>Link</em> should be a URL pointing to the item.
132 %%% <p>In case another database backend is used, the
133 %%% <em>Tag</em> has the format: <em>{DbModule, OpaqueTag}</em>
134 %%% where <em>DbModule</em> is the database backend module
135 %%% to be called, and <em>OpaqueTag</em> the Tag that is
136 %%% used in <em>DbModule:insert(Tag, ...)</em></p>
137 %%% @end
138 %%%
139 insert(Tag, Title, Link, Desc) ->
140 insert(Tag, Title, Link, Desc, "").
141
142 %%%
143 %%% @spec insert(Tag::atom(), Title::string(),
144 %%% Link::string(), Desc::string(),
145 %%% Creator::string()) ->
146 %%% ok | {error, string()}
147 %%%
148 %%% @doc Insert an RSS item into the <em>Tag</em> RSS feed.
149 %%% <em>Link</em> should be a URL pointing to the item.
150 %%%
151 insert(Tag, Title, Link, Desc, Creator) ->
152 GregSecs = calendar:datetime_to_gregorian_seconds({date(),time()}),
153 insert(Tag, Title, Link, Desc, Creator, GregSecs).
154
155 %%%
156 %%% @spec insert(Tag::atom(), Title::string(),
157 %%% Link::string(), Desc::string(),
158 %%% Creator::string(), GregSecs::integer()) ->
159 %%% ok | {error, string()}
160 %%%
161 %%% @doc Insert an RSS item into the <em>Tag</em> RSS feed.
162 %%% <em>Link</em> should be a URL pointing to the item.
163 %%% <em>GregSecs</em> is the creation time of the item
164 %%% in Gregorian Seconds.
165 %%%
166 insert(Tag, Title, Link, Desc, Creator, GregSecs) ->
167 Args = {Tag, Title, Link, Desc, Creator, GregSecs},
168 gen_server:call(?SERVER, {insert, Args}, infinity).
169
170
171 %%%
172 %%% @spec retrieve(Tag::atom()) ->
173 %%% {ok, RSScontent::IoList()} | {error, string()}
174 %%%
175 %%% @type IoList. A deep list of strings and/or binaries.
176 %%%
177 %%% @doc Retrieve the <em>RSScontent</em> (in XML and all...)
178 %%% to be delivered to a RSS client.
179 %%% <p>In case another database backend is used, the
180 %%% <em>Tag</em> has the format: <em>{DbModule, OpaqueTag}</em>
181 %%% where <em>DbModule</em> is the database backend module
182 %%% to be called, and <em>OpaqueTag</em> the Tag that is
183 %%% used in <em>DbModule:retrieve(Tag)</em> which must return
184 %%% a list of tuples: <em>{Title, Link, Desc, Creator, GregSecs}</em></p>
185 %%%
186 retrieve(Tag) ->
187 gen_server:call(?SERVER, {retrieve, Tag}, infinity).
188
189 %%%----------------------------------------------------------------------
190 %%% Callback functions from gen_server
191 %%%----------------------------------------------------------------------
192
193 %%----------------------------------------------------------------------
194 %% Func: init/1
195 %% Returns: {ok, State} |
196 %% {ok, State, Timeout} |
197 %% ignore |
198 %% {stop, Reason}
199 %%----------------------------------------------------------------------
200 init([]) ->
201 {ok, #s{}}.
202
203 %%----------------------------------------------------------------------
204 %% Func: handle_call/3
205 %% Returns: {reply, Reply, State} |
206 %% {reply, Reply, State, Timeout} |
207 %% {noreply, State} |
208 %% {noreply, State, Timeout} |
209 %% {stop, Reason, Reply, State} | (terminate/2 is called)
210 %% {stop, Reason, State} (terminate/2 is called)
211 %%----------------------------------------------------------------------
212 handle_call({open, Opts}, _From, State) ->
213 {NewState, Res} = do_open_dir(State, Opts),
214 {reply, Res, NewState};
215 %%
216 handle_call({close, DB}, _From, State) ->
217 dets:close(DB),
218 {reply, ok, State};
219 %%
220 handle_call({close, DBMod, DBname}, _From, State) ->
221 catch apply(DBMod, close, [DBname]),
222 {reply, ok, State};
223 %%
224 handle_call({insert, Args}, _From, State) ->
225 {NewState, Res} = do_insert(State, Args),
226 {reply, Res, NewState};
227 %%
228 handle_call({retrieve, Tag}, _From, State) ->
229 {NewState, Res} = do_retrieve(State, Tag),
230 {reply, Res, NewState}.
231
232 %%----------------------------------------------------------------------
233 %% Func: handle_cast/2
234 %% Returns: {noreply, State} |
235 %% {noreply, State, Timeout} |
236 %% {stop, Reason, State} (terminate/2 is called)
237 %%----------------------------------------------------------------------
238 handle_cast(_Msg, State) ->
239 {noreply, State}.
240
241 %%----------------------------------------------------------------------
242 %% Func: handle_info/2
243 %% Returns: {noreply, State} |
244 %% {noreply, State, Timeout} |
245 %% {stop, Reason, State} (terminate/2 is called)
246 %%----------------------------------------------------------------------
247 handle_info(_Info, State) ->
248 {noreply, State}.
249
250 %%----------------------------------------------------------------------
251 %% Func: terminate/2
252 %% Purpose: Shutdown the server
253 %% Returns: any (ignored by gen_server)
254 %%----------------------------------------------------------------------
255 terminate(_Reason, _State) ->
256 ok.
257
258 %%%----------------------------------------------------------------------
259 %%% Internal functions
260 %%%----------------------------------------------------------------------
261
262 %%%
263 %%% Check what database store that should be used.
264 %%% Per default 'dets' is used.
265 %%%
266 do_open_dir(State, Opts) ->
267 case get_db_mod(Opts, dets) of
268 dets ->
269 DefFile = yaws_config:yaws_dir() ++ "/" ++ a2l(?DB) ++ ".dets",
270 File = get_db_file(Opts, DefFile),
271 Expire = get_expire(Opts, #s.expire),
272 Max = get_max(Opts, #s.max),
273 Days = get_days(Opts, #s.days),
274 RmExp = get_rm_exp(Opts, #s.rm_exp),
275 case dets:is_dets_file(File) of
276 false ->
277 {State, {error, "not a proper dets file"}};
278 _ ->
279 case catch dets:open_file(?DB, [{file, File}]) of
280 {ok,_DB} = Res ->
281 {State#s{expire = Expire,
282 days = Days,
283 rm_exp = RmExp,
284 max = Max},
285 Res};
286 {error, _Reason} ->
287 {State, {error, "open dets file"}}
288 end
289 end;
290 DBmod ->
291 {State, catch apply(DBmod, open, Opts)}
292 end.
293
294
295 do_insert(State, {{DbMod,Tag}, Title, Link, Desc, Creator, GregSecs}) ->
296 {State, catch apply(DbMod, insert, [Tag,Title,Link,Desc,Creator,GregSecs])};
297 do_insert(State, {Tag, Title, Link, Desc, Creator, GregSecs}) ->
298 Counter = if (State#s.max > 0) ->
299 (State#s.counter + 1) rem State#s.max;
300 true ->
301 State#s.counter + 1
302 end,
303 Item = {Title, Link, Desc, Creator, GregSecs},
304 Res = dets:insert(?DB, ?ITEM(Tag, Counter, Item)),
305 {State#s{counter = Counter}, Res}.
306
307
308 do_retrieve(State, {DbMod,Tag}) ->
309 {State, catch apply(DbMod, retrieve, [Tag])};
310 do_retrieve(State, Tag) ->
311 F = fun(?ITEM(X, _Counter, Item), Acc) when X == Tag -> [Item|Acc];
312 (_, Acc) -> Acc
313 end,
314 Items = sort_items(expired(State, dets:foldl(F, [], ?DB))),
315 io:format("GOT ITEMS: ~p~n", [Items]),
316 Xml = to_xml(Items),
317 {State, {ok, Xml}}.
318
319
320 -define(ONE_DAY, 86400). % 24*60*60 seconds
321 -define(X(GregSecs), {Title, Link, Desc, Creator, GregSecs}).
322
323 %%% Filter away expired items !!
324 expired(State, List) when State#s.expire == days ->
325 Gs = calendar:datetime_to_gregorian_seconds({date(),time()}),
326 Old = Gs - (?ONE_DAY * State#s.days),
327 F = fun(?X(GregSecs), Acc) when GregSecs > Old ->
328 [?X(GregSecs) | Acc];
329 (_, Acc) ->
330 Acc
331 end,
332 lists:foldl(F, [], List);
333 expired(_State, List) ->
334 List.
335
336 -undef(X).
337
338
339
340 %%%
341 %%% Sort on creation date !!
342 %%% Item = {Title, Link, Desc, Creator, GregSecs},
343 %%%
344 sort_items(Is) ->
345 lists:keysort(5,Is).
346
347
348 to_xml([{Title, Link, Desc, Creator, GregSecs}|Tail]) ->
349 {{Y,M,D},_} = calendar:gregorian_seconds_to_datetime(GregSecs),
350 Date = i2l(Y) ++ "-" ++ i2l(M) ++ "-" ++ i2l(D),
351 [["<item>\n",
352 "<title>", Title, "</title>\n",
353 "<link>", Link, "</link>\n",
354 "<description>", Desc, "</description>\n",
355 "<dc:creator>", Creator, "</dc:creator>\n",
356 "<dc:date>", Date, "</dc:date>\n",
357 "</item>\n"] |
358 to_xml(Tail)];
359 to_xml([]) ->
360 [].
361
362
363 get_db_mod(Opts, Def) -> lkup(db_mod, Opts, Def).
364 get_db_file(Opts, Def) -> lkup(db_file, Opts, Def).
365 get_expire(Opts, Def) -> lkup(expire, Opts, Def).
366 get_max(Opts, Def) -> lkup(max, Opts, Def).
367 get_days(Opts, Def) -> lkup(days, Opts, Def).
368 get_rm_exp(Opts, Def ) -> lkup(rm_exp, Opts, Def).
369
370 lkup(Key, List, Def) ->
371 case lists:keysearch(Key, 1, List) of
372 {value,{_,Value}} -> Value;
373 _ -> Def
374 end.
375
376
377
378 i2l(I) when integer(I) -> integer_to_list(I);
379 i2l(L) when list(L) -> L.
380
381 a2l(A) when atom(A) -> atom_to_list(A);
382 a2l(L) when list(L) -> L.
383
384
385
386
387 t_setup() ->
388 open([{db_file, "yaws_rss.dets"}, {max,7}]),
389 insert(xml,"Normalizing XML, Part 2",
390 "http://www.xml.com/pub/a/2002/12/04/normalizing.html",
391 "In this second and final look at applying relational "
392 "normalization techniques to W3C XML Schema data modeling, "
393 "Will Provost discusses when not to normalize, the scope "
394 "of uniqueness and the fourth and fifth normal forms."),
395 insert(xml,"The .NET Schema Object Model",
396 "http://www.xml.com/pub/a/2002/12/04/som.html",
397 "Priya Lakshminarayanan describes in detail the use of "
398 "the .NET Schema Object Model for programmatic manipulation "
399 "of W3C XML Schemas."),
400 insert(xml,"SVG's Past and Promising Future",
401 "http://www.xml.com/pub/a/2002/12/04/svg.html",
402 "In this month's SVG column, Antoine Quint looks back at "
403 "SVG's journey through 2002 and looks forward to 2003.").
404
405
406 t_exp() ->
407 %%open([{db_file, "yaws_rss.dets"}, {expire,days}]),
408 insert(xml,"Expired article",
409 "http://www.xml.com/pub/a/2002/12/04/normalizing.html",
410 "In this second and final look at applying relational "
411 "normalization techniques to W3C XML Schema data modeling, "
412 "Will Provost discusses when not to normalize, the scope "
413 "of uniqueness and the fourth and fifth normal forms.",
414 "tobbe",
415 63269561882). % 6/12-2004
416
417 t_xopen() ->
418 open([{db_file, "yaws_rss.dets"},
419 {expire,days},
420 {days, 20}]).
Something went wrong with that request. Please try again.