source: wiki-toolkit/trunk/lib/Wiki/Toolkit/Feed/Atom.pm @ 329

Last change on this file since 329 was 329, checked in by nick, 14 years ago

Refactor, and add support for a minimal kind of feed (eg as would come from a search). Also include more geo data in the feed

  • Property svn:executable set to *
File size: 12.2 KB
Line 
1package Wiki::Toolkit::Feed::Atom;
2
3use strict;
4
5use vars qw( @ISA $VERSION );
6$VERSION = '0.01';
7
8use POSIX 'strftime';
9use Time::Piece;
10use URI::Escape;
11use Carp qw( croak );
12
13use Wiki::Toolkit::Feed::Listing;
14@ISA = qw( Wiki::Toolkit::Feed::Listing );
15
16sub new
17{
18  my $class = shift;
19  my $self  = {};
20  bless $self, $class;
21
22  my %args = @_;
23  my $wiki = $args{wiki};
24
25  unless ($wiki && UNIVERSAL::isa($wiki, 'Wiki::Toolkit'))
26  {
27    croak 'No Wiki::Toolkit object supplied';
28  }
29 
30  $self->{wiki} = $wiki;
31 
32  # Mandatory arguments.
33  foreach my $arg (qw/site_name site_url make_node_url recent_changes_link atom_link/)
34  {
35    croak "No $arg supplied" unless $args{$arg};
36    $self->{$arg} = $args{$arg};
37  }
38 
39  # Optional arguments.
40  foreach my $arg (qw/site_description software_name software_version software_homepage/)
41  {
42    $self->{$arg} = $args{$arg} || '';
43  }
44
45  $self->{timestamp_fmt} = $Wiki::Toolkit::Store::Database::timestamp_fmt;
46  $self->{utc_offset} = strftime "%z", localtime;
47  $self->{utc_offset} =~ s/(..)(..)$/$1:$2/;
48 
49  $self;
50}
51
52=item <build_feed_start>
53
54Internal method, to build all the stuff that will go at the start of a feed.
55Outputs the feed header, and initial feed info.
56
57=cut
58sub build_feed_start {
59  my ($self,$atom_timestamp) = @_;
60
61  my $generator = '';
62 
63  if ($self->{software_name})
64  {
65    $generator  = '  <generator';
66    $generator .= ' uri="' . $self->{software_homepage} . '"'   if $self->{software_homepage};
67    $generator .= ' version=' . $self->{software_version} . '"' if $self->{software_version};
68    $generator .= ">\n";
69    $generator .= $self->{software_name} . "</generator>\n";
70  }                         
71
72  my $subtitle = $self->{site_description}
73                 ? '<subtitle>' . $self->{site_description} . "</subtitle>\n"
74                 : '';
75                 
76  my $atom = qq{<?xml version="1.0" encoding="UTF-8"?>
77
78<feed
79 xmlns         = "http://www.w3.org/2005/Atom"
80 xmlns:geo     = "http://www.w3.org/2003/01/geo/wgs84_pos#"
81 xmlns:space   = "http://frot.org/space/0.1/"
82>
83
84  <link href="}            . $self->{site_url}     . qq{" />
85  <title>}                 . $self->{site_name}    . qq{</title>
86  <link rel="self" href="} . $self->{atom_link}    . qq{" />
87  <updated>}               . $atom_timestamp       . qq{</updated>
88  <id>}                    . $self->{site_url}     . qq{</id>
89  $subtitle};
90 
91  return $atom;
92}
93
94=item <build_feed_end>
95
96Internal method, to build all the stuff that will go at the end of a feed.
97
98=cut
99sub build_feed_end {
100    my ($self,$feed_timestamp) = @_;
101
102    return "</feed>\n";
103}
104
105=item <generate_node_list_feed>
106 
107Generate and return an Atom feed for a list of nodes
108 
109=cut
110sub generate_node_list_feed {
111  my ($self,$atom_timestamp,@nodes) = @_;
112
113  my $atom = $self->build_feed_start($atom_timestamp);
114
115  my (@urls, @items);
116
117  foreach my $node (@nodes)
118  {
119    my $node_name = $node->{name};
120
121    my $item_timestamp = $node->{last_modified};
122   
123    # Make a Time::Piece object.
124    my $time = Time::Piece->strptime($item_timestamp, $self->{timestamp_fmt});
125
126    my $utc_offset = $self->{utc_offset};
127   
128    $item_timestamp = $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
129
130    my $author      = $node->{metadata}{username}[0] || $node->{metadata}{host}[0] || 'Anonymous';
131    my $description = $node->{metadata}{comment}[0]  || 'No description given for node';
132
133    $description .= " [$author]" if $author;
134
135    my $version = $node->{version};
136    my $status  = (1 == $version) ? 'new' : 'updated';
137
138    my $major_change = $node->{metadata}{major_change}[0];
139       $major_change = 1 unless defined $major_change;
140    my $importance = $major_change ? 'major' : 'minor';
141
142    my $url = $self->{make_node_url}->($node_name, $version);
143
144    # make XML-clean
145    my $title =  $node_name;
146       $title =~ s/&/&amp;/g;
147       $title =~ s/</&lt;/g;
148       $title =~ s/>/&gt;/g;
149
150    # Pop the categories into atom:category elements (4.2.2)
151    # We can do this because the spec says:
152    #   "This specification assigns no meaning to the content (if any)
153    #    of this element."
154    # TODO: Decide if we should include the "all categories listing" url
155    #        as the scheme (URI) attribute?
156    my $category_atom = "";
157    if($node->{metadata}->{category}) {
158        foreach my $cat (@{ $node->{metadata}->{category} }) {
159            $category_atom .= "    <category term=\"$cat\" />\n";
160        }
161    }
162
163    # Include geospacial data, if we have it
164    my $geo_atom = "";
165    if($node->{metadata}->{latitude}) {
166        $geo_atom .= "  <geo:lat>".$node->{metadata}->{latitude}."</geo:lat>\n";
167    }
168    if($node->{metadata}->{longitude}) {
169        $geo_atom .= "  <geo:long>".$node->{metadata}->{longitude}."</geo:long>\n";
170    }
171    if($node->{metadata}->{os_x}) {
172        $geo_atom .= "  <space:os_x>".$node->{metadata}->{os_x}."</space:os_x>\n";
173    }
174    if($node->{metadata}->{os_y}) {
175        $geo_atom .= "  <space:os_y>".$node->{metadata}->{os_y}."</space:os_y>\n";
176    }
177    if($node->{metadata}->{distance}) {
178        $geo_atom .= "  <space:distance>".$node->{metadata}->{distance}."</space:distance>\n";
179    }
180
181    # TODO: Find an Atom equivalent of ModWiki, so we can include more info
182
183   
184    push @items, qq{
185  <entry>
186    <title>$title</title>
187    <link href="$url" />
188    <id>$url</id>
189    <summary>$description</summary>
190    <updated>$item_timestamp</updated>
191    <author><name>$author</name></author>
192$category_atom
193$geo_atom
194  </entry>
195};
196
197  }
198 
199  $atom .= join('', @items) . "\n";
200  $atom .= $self->build_feed_end($atom_timestamp);
201
202  return $atom;   
203}
204
205=item <generate_node_name_distance_feed>
206 
207Generate a very cut down atom feed, based just on the nodes, their locations
208(if given), and their distance from a reference location (if given).
209
210Typically used on search feeds.
211 
212=cut
213sub generate_node_name_distance_feed {
214  my ($self,$atom_timestamp,@nodes) = @_;
215
216  my $atom = $self->build_feed_start($atom_timestamp);
217
218  my (@urls, @items);
219
220  foreach my $node (@nodes)
221  {
222    my $node_name = $node->{name};
223
224    my $url = $self->{make_node_url}->($node_name);
225
226    # make XML-clean
227    my $title =  $node_name;
228       $title =~ s/&/&amp;/g;
229       $title =~ s/</&lt;/g;
230       $title =~ s/>/&gt;/g;
231
232    push @items, qq{
233  <entry>
234    <title>$title</title>
235    <link href="$url" />
236    <id>$url</id>
237  </entry>
238};
239
240  }
241 
242  $atom .= join('', @items) . "\n";
243  $atom .= $self->build_feed_end($atom_timestamp);
244
245  return $atom;   
246}
247
248=item B<feed_timestamp>
249
250Generate the timestamp for the Atom, based on the newest node (if available)
251
252=cut
253sub feed_timestamp
254{
255  my ($self, $newest_node) = @_;
256 
257  if ($newest_node->{last_modified})
258  {
259    my $time = Time::Piece->strptime( $newest_node->{last_modified}, $self->{timestamp_fmt} );
260
261    my $utc_offset = $self->{utc_offset};
262   
263    return $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
264  }
265  else
266  {
267    return '1970-01-01T00:00:00+0000';
268  }
269}
270
2711;
272
273__END__
274
275=head1 NAME
276
277  Wiki::Toolkit::Feed::Atom - A Wiki::Toolkit plugin to output RecentChanges Atom.
278
279=head1 DESCRIPTION
280
281This is an alternative access to the recent changes of a Wiki::Toolkit
282wiki. It outputs the Atom Syndication Format as described at
283L<http://www.atomenabled.org/developers/syndication/>.
284
285This module is a straight port of L<Wiki::Toolkit::Feed::RSS>.
286
287=head1 SYNOPSIS
288
289  use Wiki::Toolkit;
290  use Wiki::Toolkit::Feed::Atom;
291
292  my $wiki = Wiki::Toolkit->new( ... );  # See perldoc Wiki::Toolkit
293
294  # Set up the RSS feeder with the mandatory arguments - see
295  # C<new()> below for more, optional, arguments.
296  my $atom = Wiki::Toolkit::Feed::Atom->new(
297    wiki                => $wiki,
298    site_name           => 'My Wiki',
299    site_url            => 'http://example.com/',
300    make_node_url       => sub
301                           {
302                             my ($node_name, $version) = @_;
303                             return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
304                           },
305    recent_changes_link => 'http://example.com/?RecentChanges',
306    atom_link => 'http://example.com/?action=rc;format=atom',
307  );
308
309  print "Content-type: application/atom+xml\n\n";
310  print $atom->recent_changes;
311
312=head1 METHODS
313
314=head2 C<new()>
315
316  my $atom = Wiki::Toolkit::Feed::Atom->new(
317    # Mandatory arguments:
318    wiki                 => $wiki,
319    site_name            => 'My Wiki',
320    site_url             => 'http://example.com/',
321    make_node_url        => sub
322                            {
323                              my ($node_name, $version) = @_;
324                              return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
325                            },
326    recent_changes_link  => 'http://example.com/?RecentChanges',,
327    atom_link => 'http://example.com/?action=rc;format=atom',
328
329    # Optional arguments:
330    site_description     => 'My wiki about my stuff',
331    software_name        => $your_software_name,     # e.g. "Wiki::Toolkit"
332    software_version     => $your_software_version,  # e.g. "0.73"
333    software_homepage    => $your_software_homepage, # e.g. "http://search.cpan.org/dist/CGI-Wiki/"
334  );
335
336C<wiki> must be a L<Wiki::Toolkit> object. C<make_node_url>, if supplied, must
337be a coderef.
338
339The mandatory arguments are:
340
341=over 4
342
343=item * wiki
344
345=item * site_name
346
347=item * site_url
348
349=item * make_node_url
350
351=item * recent_changes_link
352
353=item * atom_link
354
355=back
356
357The three optional arguments
358
359=over 4
360
361=item * software_name
362
363=item * software_version
364
365=item * software_homepage
366
367=back
368
369are used to generate the C<generator> part of the feed.
370
371=head2 C<recent_changes()>
372
373  $wiki->write_node(
374                     'About This Wiki',
375                     'blah blah blah',
376                                 $checksum,
377                           {
378                       comment  => 'Stub page, please update!',
379                                   username => 'Fred',
380                     }
381  );
382
383  print "Content-type: application/atom+xml\n\n";
384  print $atom->recent_changes;
385
386  # Or get something other than the default of the latest 15 changes.
387  print $atom->recent_changes( items => 50 );
388  print $atom->recent_changes( days => 7 );
389
390  # Or ignore minor edits.
391  print $atom->recent_changes( ignore_minor_edits => 1 );
392
393  # Personalise your feed further - consider only changes
394  # made by Fred to pages about bookshops.
395  print $atom->recent_changes(
396             filter_on_metadata => {
397                         username => 'Fred',
398                         category => 'Bookshops',
399                       },
400              );
401
402If using C<filter_on_metadata>, note that only changes satisfying
403I<all> criteria will be returned.
404
405B<Note:> Many of the fields emitted by the Atom generator are taken
406from the node metadata. The form of this metadata is I<not> mandated
407by L<Wiki::Toolkit>. Your wiki application should make sure to store some or
408all of the following metadata when calling C<write_node>:
409
410=over 4
411
412=item B<comment> - a brief comment summarising the edit that has just been made; will be used in the summary for this item.  Defaults to the empty string.
413
414=item B<username> - an identifier for the person who made the edit; will be used as the Dublin Core contributor for this item, and also in the RDF description.  Defaults to 'No description given for change'.
415
416=item B<host> - the hostname or IP address of the computer used to make the edit; if no username is supplied then this will be used as the author for this item.  Defaults to 'Anonymous'.
417
418=back
419
420=head2 C<feed_timestamp()>
421
422  print $atom->feed_timestamp();
423
424Returns the timestamp of the feed in POSIX::strftime style ("Tue, 29 Feb 2000
42512:34:56 GMT"), which is equivalent to the timestamp of the most recent item
426in the feed. Takes the same arguments as recent_changes(). You will most likely
427need this to print a Last-Modified HTTP header so user-agents can determine
428whether they need to reload the feed or not.
429 
430=head1 SEE ALSO
431
432=over 4
433
434=item * L<Wiki::Toolkit>
435
436=item * L<http://www.atomenabled.org/developers/syndication/>
437
438=back
439
440=head1 MAINTAINER
441
442The Wiki::Toolkit team, http://www.wiki-toolkit.org/.
443
444=head1 COPYRIGHT AND LICENSE
445
446Copyright 2006 Earle Martin and the Wiki::Toolkit team.
447
448This module is free software; you can redistribute it and/or modify it
449under the same terms as Perl itself.
450
451=head1 THANKS
452
453Kake Pugh for originally writing Wiki::Toolkit::Feed::RSS and indeed
454Wiki::Toolkit itself.
455
456=cut
Note: See TracBrowser for help on using the repository browser.