source: trunk/lib/OpenGuides/RDF.pm @ 703

Last change on this file since 703 was 703, checked in by Earle Martin, 16 years ago

DOAP metadata for RSS feed from new CGI::Wiki::Plugin::RSS

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 12.5 KB
Line 
1package OpenGuides::RDF;
2
3use strict;
4
5use vars qw( $VERSION );
6$VERSION = '0.08';
7
8use CGI::Wiki::Plugin::RSS::ModWiki;
9use Time::Piece;
10use URI::Escape;
11use Carp 'croak';
12
13sub new {
14    my ($class, @args) = @_;
15    my $self = {};
16    bless $self, $class;
17    $self->_init(@args);
18}
19
20sub _init {
21    my ($self, %args) = @_;
22
23    my $wiki = $args{wiki};
24   
25    unless ( $wiki && UNIVERSAL::isa( $wiki, "CGI::Wiki" ) ) {
26      croak "No CGI::Wiki object supplied.";
27    }
28    $self->{wiki} = $wiki;
29
30    my $config = $args{config};
31
32    unless ( $config && UNIVERSAL::isa( $config, "OpenGuides::Config" ) ) {
33        croak "No OpenGuides::Config object supplied.";
34    }
35    $self->{config} = $config;
36
37    $self->{make_node_url} = sub {
38        my ($node_name, $version) = @_;
39
40        my $config = $self->{config};
41   
42        my $node_url = $config->script_url . uri_escape($config->script_name) . '?';
43        $node_url .= 'id=' if defined $version;
44        $node_url .= uri_escape($self->{wiki}->formatter->node_name_to_node_param($node_name));
45        $node_url .= ';version=' . uri_escape($version) if defined $version;
46
47        $node_url;
48      }; 
49    $self->{site_name}        = $config->site_name;
50    $self->{default_city}     = $config->default_city     || "";
51    $self->{default_country}  = $config->default_country  || "";
52    $self->{site_description} = $config->site_desc        || "";
53    $self->{og_version}       = $args{og_version};
54
55    $self;
56}
57
58sub emit_rdfxml {
59    my ($self, %args) = @_;
60
61    my $node_name = $args{node};
62    my $wiki = $self->{wiki};
63
64    my %node_data          = $wiki->retrieve_node( $node_name );
65    my $phone              = $node_data{metadata}{phone}[0]              || '';
66    my $fax                = $node_data{metadata}{fax}[0]                || '';
67    my $website            = $node_data{metadata}{website}[0]            || '';
68    my $opening_hours_text = $node_data{metadata}{opening_hours_text}[0] || '';
69    my $address            = $node_data{metadata}{address}[0]            || '';
70    my $postcode           = $node_data{metadata}{postcode}[0]           || '';
71    my $city               = $node_data{metadata}{city}[0]               || $self->{default_city};
72    my $country            = $node_data{metadata}{country}[0]            || $self->{default_country};
73    my $latitude           = $node_data{metadata}{latitude}[0]           || '';
74    my $longitude          = $node_data{metadata}{longitude}[0]          || '';
75    my $version            = $node_data{version};
76    my $username           = $node_data{metadata}{username}[0]           || '';
77    my $os_x               = $node_data{metadata}{os_x}[0]               || '';
78    my $os_y               = $node_data{metadata}{os_y}[0]               || '';
79    my @categories         = @{ $node_data{metadata}{category} || [] };
80    my @locales            = @{ $node_data{metadata}{locale} || [] };
81    my $summary            = $node_data{metadata}{summary}[0]            || '';
82
83    # replace any errant characters in data to prevent illegal XML
84    foreach ($phone, $fax, $website, $opening_hours_text, $address, $postcode, 
85             $city, $country, $latitude, $longitude, $version, $os_x, $os_y, 
86             @categories, @locales, $summary)
87    {
88      if ($_)
89      {
90        $_ =~ s/&/&/g;
91        $_ =~ s/</&lt;/g;
92        $_ =~ s/>/&gt;/g;
93      }
94    }
95   
96    my ($is_geospatial, $objType);
97
98    if ($os_x || $os_y || $latitude || $longitude || $address || $postcode || @locales) {
99        $is_geospatial = 1;
100        $objType    = 'geo:SpatialThing';
101    } else {
102        $objType = 'rdf:Description';
103    }
104
105    my $timestamp = $node_data{last_modified};
106   
107    # Make a Time::Piece object.
108    my $timestamp_fmt = $CGI::Wiki::Store::Database::timestamp_fmt;
109
110    if ( $timestamp ) {
111        my $time   = Time::Piece->strptime($timestamp, $timestamp_fmt);
112        $timestamp = $time->strftime("%Y-%m-%dT%H:%M:%S");
113    }
114
115    my $url               = $self->{make_node_url}->( $node_name, $version );
116    my $version_indpt_url = $self->{make_node_url}->( $node_name );
117
118    my $rdf = qq{<?xml version="1.0"?>
119<rdf:RDF
120  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
121  xmlns:dc="http://purl.org/dc/elements/1.1/"
122  xmlns:dcterms="http://purl.org/dc/terms/"
123  xmlns:foaf="http://xmlns.com/foaf/0.1/"
124  xmlns:wiki="http://purl.org/rss/1.0/modules/wiki/"
125  xmlns:chefmoz="http://chefmoz.org/rdf/elements/1.0/"
126  xmlns:wn="http://xmlns.com/wordnet/1.6/"
127  xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
128  xmlns:os="http://downlode.org/rdf/os/0.1/"
129  xmlns:owl="http://www.w3.org/2002/07/owl#"
130  xmlns="http://www.w3.org/2000/10/swap/pim/contact#"
131>
132
133  <rdf:Description rdf:about="">
134    <dc:title>} . $self->{site_name} . qq{: $node_name</dc:title>
135    <dc:date>$timestamp</dc:date>
136    <dcterms:modified>$timestamp</dcterms:modified>
137    <dc:contributor>$username</dc:contributor>
138    <dc:source rdf:resource="$version_indpt_url" />
139    <wiki:version>$version</wiki:version>
140    <foaf:topic rdf:resource="#obj" />
141  </rdf:Description>
142
143  <$objType rdf:ID="obj" dc:title="$node_name">
144};
145    $rdf .= "    <dc:description>$summary</dc:description>\n" if $summary;
146
147    $rdf .= "\n    <!-- categories -->\n\n" if @categories;
148    $rdf .= "    <dc:subject>$_</dc:subject>\n" foreach @categories;
149   
150    if ($is_geospatial)
151    {
152      $rdf .= "\n    <!-- address and geospatial data -->\n\n";
153      $rdf .= "    <address>$address</address>\n"        if $address;
154      $rdf .= "    <city>$city</city>\n"                 if $city;
155      $rdf .= "    <postalCode>$postcode</postalCode>\n" if $postcode;
156      $rdf .= "    <country>$country</country>\n"        if $country;
157
158      $rdf .= qq{
159    <foaf:based_near>
160      <wn:Neighborhood>
161        <foaf:name>$_</foaf:name>
162      </wn:Neighborhood>
163    </foaf:based_near>\n} foreach @locales;
164
165      if ( $latitude && $longitude ) {
166          $rdf .= qq{
167    <geo:lat>$latitude</geo:lat>
168    <geo:long>$longitude</geo:long>\n};
169      }
170
171      if ( $os_x && $os_y ) {
172          $rdf .= qq{
173    <os:x>$os_x</os:x>
174    <os:y>$os_y</os:y>};
175      }
176    }
177   
178    $rdf .= "\n\n    <!-- contact information -->\n\n" if ($phone || $fax || $website || $opening_hours_text);
179    $rdf .= "    <phone>$phone</phone>\n"                              if $phone;
180    $rdf .= "    <fax>$fax</fax>\n"                                    if $fax;
181    $rdf .= "    <foaf:homepage rdf:resource=\"$website\" />\n"        if $website;
182    $rdf .= "    <chefmoz:Hours>$opening_hours_text</chefmoz:Hours>\n" if $opening_hours_text;
183
184    if ($node_data{content} =~ /^\#REDIRECT \[\[(.*?)]\]$/)
185    {
186      my $redirect = $1;
187     
188      $rdf .= qq{    <owl:sameAs rdf:resource="} . $self->{config}->script_url
189      . uri_escape($self->{config}->script_name) . '?id='
190      . uri_escape($wiki->formatter->node_name_to_node_param($redirect))
191      . ';format=rdf#obj';
192      $rdf .= qq{" />\n};
193    }
194   
195    $rdf .= qq{  </$objType>
196</rdf:RDF>
197
198};
199
200    return $rdf;
201}
202
203sub rss_maker {
204    my $self = shift;
205
206    # OAOO, please.
207    unless ($self->{rss_maker}) {
208        $self->{rss_maker} = CGI::Wiki::Plugin::RSS::ModWiki->new(
209          wiki                => $self->{wiki},
210          site_name           => $self->{site_name},
211          site_url            => $self->{config}->script_url,
212          site_description    => $self->{site_description},
213          make_node_url       => $self->{make_node_url},
214          recent_changes_link => $self->{config}->script_url . '?action=rss',
215          software_name       => 'OpenGuides',
216          software_homepage   => 'http://openguides.org/',
217          software_version    => $self->{og_version},
218        );
219    }
220   
221    $self->{rss_maker};
222}
223
224sub make_recentchanges_rss {
225    my ($self, %args) = @_;
226
227    $self->rss_maker->recent_changes(%args);
228}
229
230sub rss_timestamp {
231    my ($self, %args) = @_;
232   
233    $self->rss_maker->rss_timestamp(%args);
234}
235
236=head1 NAME
237
238OpenGuides::RDF - An OpenGuides plugin to output RDF/XML.
239
240=head1 DESCRIPTION
241
242Does all the RDF stuff for OpenGuides.  Distributed and installed as
243part of the OpenGuides project, not intended for independent
244installation.  This documentation is probably only useful to OpenGuides
245developers.
246
247=head1 SYNOPSIS
248
249    use CGI::Wiki;
250    use OpenGuides::Config;
251    use OpenGuides::RDF;
252
253    my $wiki = CGI::Wiki->new( ... );
254    my $config = OpenGuides::Config->new( file => "wiki.conf" );
255    my $rdf_writer = OpenGuides::RDF->new( wiki   => $wiki,
256                                         config => $config );
257
258    # RDF version of a node.
259    print "Content-Type: text/plain\n\n";
260    print $rdf_writer->emit_rdfxml( node => "Masala Zone, N1 0NU" );
261
262    # Ten most recent changes.
263    print "Content-Type: text/plain\n";
264    print "Last-Modified: " . $self->rss_timestamp( items => 10 ) . "\n\n";
265    print $rdf_writer->make_recentchanges_rss( items => 10 );
266
267=head1 METHODS
268
269=over 4
270
271=item B<new>
272
273    my $rdf_writer = OpenGuides::RDF->new( wiki   => $wiki,
274                                           config => $config );
275
276C<wiki> must be a L<CGI::Wiki> object and C<config> must be an
277L<OpenGuides::Config> object.  Both arguments mandatory.
278
279
280=item B<emit_rdfxml>
281
282    $wiki->write_node( "Masala Zone, N1 0NU",
283                     "Quick and tasty Indian food",
284                     $checksum,
285                     { comment  => "New page",
286                       username => "Kake",
287                       locale   => "Islington" }
288    );
289
290    print "Content-Type: text/plain\n\n";
291    print $rdf_writer->emit_rdfxml( node => "Masala Zone, N1 0NU" );
292
293B<Note:> Some of the fields emitted by the RDF/XML generator are taken
294from the node metadata. The form of this metadata is I<not> mandated
295by L<CGI::Wiki>. Your wiki application should make sure to store some or
296all of the following metadata when calling C<write_node>:
297
298=over 4
299
300=item B<postcode> - The postcode or zip code of the place discussed by the node.  Defaults to the empty string.
301
302=item B<city> - The name of the city that the node is in.  If not supplied, then the value of C<default_city> in the config object supplied to C<new>, if available, otherwise the empty string.
303
304=item B<country> - The name of the country that the node is in.  If not supplied, then the value of C<default_country> in the config object supplied to C<new> will be used, if available, otherwise the empty string.
305
306=item B<username> - An identifier for the person who made the latest edit to the node.  This person will be listed as a contributor (Dublin Core).  Defaults to empty string.
307
308=item B<locale> - The value of this can be a scalar or an arrayref, since some places have a plausible claim to being in more than one locale.  Each of these is put in as a C<Neighbourhood> attribute.
309
310=item B<phone> - Only one number supported at the moment.  No validation.
311
312=item B<website> - No validation.
313
314=item B<opening_hours_text> - A freeform text field.
315
316=back
317
318=item B<rss_maker>
319
320Returns a raw L<CGI::Wiki::Plugin::RSS::ModWiki> object created with the values you
321invoked this module with.
322
323=item B<make_recentchanges_rss>
324
325    # Ten most recent changes.
326    print "Content-Type: text/plain\n";
327    print "Last-Modified: " . $rdf_writer->rss_timestamp( items => 10 ) . "\n\n";
328    print $rdf_writer->make_recentchanges_rss( items => 10 );
329
330    # All the changes made by bob in the past week, ignoring minor edits.
331
332    my %args = (
333                 days               => 7,
334                 ignore_minor_edits => 1,
335                 filter_on_metadata => { username => "bob" },
336               );
337
338    print "Content-Type: text/plain\n";
339    print "Last-Modified: " . $rdf_writer->rss_timestamp( %args ) . "\n\n";
340    print $rdf_writer->make_recentchanges_rss( %args );
341
342=item B<rss_timestamp>
343
344    print "Last-Modified: " . $rdf_writer->rss_timestamp( %args ) . "\n\n";
345
346Returns the timestamp of the RSS feed in POSIX::strftime style ("Tue, 29 Feb 2000
34712:34:56 GMT"), which is equivalent to the timestamp of the most recent item
348in the feed. Takes the same arguments as make_recentchanges_rss(). You will most
349likely need this to print a Last-Modified HTTP header so user-agents can determine
350whether they need to reload the feed or not.
351
352=back
353
354=head1 SEE ALSO
355
356=over 4
357
358=item * L<CGI::Wiki>
359
360=item * L<http://openguides.org/>
361
362=item * L<http://chefmoz.org/>
363
364=back
365
366=head1 AUTHOR
367
368The OpenGuides Project (openguides-dev@openguides.org)
369
370=head1 COPYRIGHT
371
372Copyright (C) 2003-2005 The OpenGuides Project.  All Rights Reserved.
373
374This module is free software; you can redistribute it and/or modify it
375under the same terms as Perl itself.
376
377=head1 CREDITS
378
379Code in this module written by Kake Pugh and Earle Martin.  Dan Brickley, Matt
380Biddulph and other inhabitants of #swig on irc.freenode.net gave useful feedback
381and advice.
382
383=cut
384
3851;
Note: See TracBrowser for help on using the repository browser.