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

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

fixes #51

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 12.6 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 || $opening_hours_text) {
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    my $rdf = qq{<?xml version="1.0"?>
118<rdf:RDF
119  xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
120  xmlns:dc="http://purl.org/dc/elements/1.1/"
121  xmlns:dcterms="http://purl.org/dc/terms/"
122  xmlns:foaf="http://xmlns.com/foaf/0.1/"
123  xmlns:wiki="http://purl.org/rss/1.0/modules/wiki/"
124  xmlns:chefmoz="http://chefmoz.org/rdf/elements/1.0/"
125  xmlns:wn="http://xmlns.com/wordnet/1.6/"
126  xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
127  xmlns:os="http://downlode.org/rdf/os/0.1/"
128  xmlns:owl="http://www.w3.org/2002/07/owl#"
129  xmlns="http://www.w3.org/2000/10/swap/pim/contact#"
130>
131
132  <rdf:Description rdf:about="">
133    <dc:title>} . $self->{site_name} . qq{: $node_name</dc:title>
134    <dc:date>$timestamp</dc:date>
135    <dcterms:modified>$timestamp</dcterms:modified>
136    <dc:contributor>$username</dc:contributor>
137    <dc:source rdf:resource="$version_indpt_url" />
138    <wiki:version>$version</wiki:version>
139    <foaf:topic rdf:resource="#obj" />
140  </rdf:Description>
141
142  <$objType rdf:ID="obj" dc:title="$node_name">
143};
144    $rdf .= "    <dc:description>$summary</dc:description>\n" if $summary;
145
146    $rdf .= "\n    <!-- categories -->\n\n" if @categories;
147    $rdf .= "    <dc:subject>$_</dc:subject>\n" foreach @categories;
148   
149    if ($is_geospatial)
150    {
151      $rdf .= "\n    <!-- address and geospatial data -->\n\n";
152      $rdf .= "    <address>$address</address>\n"        if $address;
153      $rdf .= "    <city>$city</city>\n"                 if $city;
154      $rdf .= "    <postalCode>$postcode</postalCode>\n" if $postcode;
155      $rdf .= "    <country>$country</country>\n"        if $country;
156
157      $rdf .= qq{
158    <foaf:based_near>
159      <wn:Neighborhood>
160        <foaf:name>$_</foaf:name>
161      </wn:Neighborhood>
162    </foaf:based_near>\n} foreach @locales;
163
164      if ( $latitude && $longitude ) {
165          $rdf .= qq{
166    <geo:lat>$latitude</geo:lat>
167    <geo:long>$longitude</geo:long>\n};
168      }
169
170      if ( $os_x && $os_y ) {
171          $rdf .= qq{
172    <os:x>$os_x</os:x>
173    <os:y>$os_y</os:y>};
174      }
175    }
176   
177    $rdf .= "\n\n    <!-- contact information -->\n\n" if ($phone || $fax || $website || $opening_hours_text);
178    $rdf .= "    <phone>$phone</phone>\n"                              if $phone;
179    $rdf .= "    <fax>$fax</fax>\n"                                    if $fax;
180    $rdf .= "    <foaf:homepage rdf:resource=\"$website\" />\n"        if $website;
181    $rdf .= "    <chefmoz:Hours>$opening_hours_text</chefmoz:Hours>\n" if $opening_hours_text;
182
183    if ($node_data{content} =~ /^\#REDIRECT \[\[(.*?)]\]$/)
184    {
185      my $redirect = $1;
186     
187      $rdf .= qq{    <owl:sameAs rdf:resource="} . $self->{config}->script_url
188      . uri_escape($self->{config}->script_name) . '?id='
189      . uri_escape($wiki->formatter->node_name_to_node_param($redirect))
190      . ';format=rdf#obj';
191      $rdf .= qq{" />\n};
192    }
193   
194    $rdf .= qq{  </$objType>
195</rdf:RDF>
196
197};
198
199    return $rdf;
200}
201
202sub rss_maker {
203    my $self = shift;
204
205    # OAOO, please.
206    unless ($self->{rss_maker}) {
207        $self->{rss_maker} = CGI::Wiki::Plugin::RSS::ModWiki->new(
208          wiki                => $self->{wiki},
209          site_name           => $self->{site_name},
210          site_url            => $self->{config}->script_url,
211          site_description    => $self->{site_description},
212          make_node_url       => $self->{make_node_url},
213          recent_changes_link => $self->{config}->script_url . '?action=rss',
214          software_name       => 'OpenGuides',
215          software_homepage   => 'http://openguides.org/',
216          software_version    => $self->{og_version},
217        );
218    }
219   
220    $self->{rss_maker};
221}
222
223sub make_recentchanges_rss {
224    my ($self, %args) = @_;
225
226    $self->rss_maker->recent_changes(%args);
227}
228
229sub rss_timestamp {
230    my ($self, %args) = @_;
231   
232    $self->rss_maker->rss_timestamp(%args);
233}
234
235=head1 NAME
236
237OpenGuides::RDF - An OpenGuides plugin to output RDF/XML.
238
239=head1 DESCRIPTION
240
241Does all the RDF stuff for OpenGuides.  Distributed and installed as
242part of the OpenGuides project, not intended for independent
243installation.  This documentation is probably only useful to OpenGuides
244developers.
245
246=head1 SYNOPSIS
247
248    use CGI::Wiki;
249    use OpenGuides::Config;
250    use OpenGuides::RDF;
251
252    my $wiki = CGI::Wiki->new( ... );
253    my $config = OpenGuides::Config->new( file => "wiki.conf" );
254    my $rdf_writer = OpenGuides::RDF->new( wiki   => $wiki,
255                                         config => $config );
256
257    # RDF version of a node.
258    print "Content-Type: application/rdf+xml\n\n";
259    print $rdf_writer->emit_rdfxml( node => "Masala Zone, N1 0NU" );
260
261    # Ten most recent changes.
262    print "Content-Type: application/rdf+xml\n";
263    print "Last-Modified: " . $self->rss_timestamp( items => 10 ) . "\n\n";
264    print $rdf_writer->make_recentchanges_rss( items => 10 );
265
266=head1 METHODS
267
268=over 4
269
270=item B<new>
271
272    my $rdf_writer = OpenGuides::RDF->new( wiki   => $wiki,
273                                           config => $config );
274
275C<wiki> must be a L<CGI::Wiki> object and C<config> must be an
276L<OpenGuides::Config> object.  Both arguments mandatory.
277
278
279=item B<emit_rdfxml>
280
281    $wiki->write_node( "Masala Zone, N1 0NU",
282                     "Quick and tasty Indian food",
283                     $checksum,
284                     { comment  => "New page",
285                       username => "Kake",
286                       locale   => "Islington" }
287    );
288
289    print "Content-Type: application/rdf+xml\n\n";
290    print $rdf_writer->emit_rdfxml( node => "Masala Zone, N1 0NU" );
291
292B<Note:> Some of the fields emitted by the RDF/XML generator are taken
293from the node metadata. The form of this metadata is I<not> mandated
294by L<CGI::Wiki>. Your wiki application should make sure to store some or
295all of the following metadata when calling C<write_node>:
296
297=over 4
298
299=item B<postcode> - The postcode or zip code of the place discussed by the node.  Defaults to the empty string.
300
301=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.
302
303=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.
304
305=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.
306
307=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.
308
309=item B<phone> - Only one number supported at the moment.  No validation.
310
311=item B<website> - No validation.
312
313=item B<opening_hours_text> - A freeform text field.
314
315=back
316
317=item B<rss_maker>
318
319Returns a raw L<CGI::Wiki::Plugin::RSS::ModWiki> object created with the values you
320invoked this module with.
321
322=item B<make_recentchanges_rss>
323
324    # Ten most recent changes.
325    print "Content-Type: application/rdf+xml\n";
326    print "Last-Modified: " . $rdf_writer->rss_timestamp( items => 10 ) . "\n\n";
327    print $rdf_writer->make_recentchanges_rss( items => 10 );
328
329    # All the changes made by bob in the past week, ignoring minor edits.
330
331    my %args = (
332                 days               => 7,
333                 ignore_minor_edits => 1,
334                 filter_on_metadata => { username => "bob" },
335               );
336
337    print "Content-Type: application/rdf+xml\n";
338    print "Last-Modified: " . $rdf_writer->rss_timestamp( %args ) . "\n\n";
339    print $rdf_writer->make_recentchanges_rss( %args );
340
341=item B<rss_timestamp>
342
343    print "Last-Modified: " . $rdf_writer->rss_timestamp( %args ) . "\n\n";
344
345Returns the timestamp of the RSS feed in POSIX::strftime style ("Tue, 29 Feb 2000
34612:34:56 GMT"), which is equivalent to the timestamp of the most recent item
347in the feed. Takes the same arguments as make_recentchanges_rss(). You will most
348likely need this to print a Last-Modified HTTP header so user-agents can determine
349whether they need to reload the feed or not.
350
351=back
352
353=head1 SEE ALSO
354
355=over 4
356
357=item * L<CGI::Wiki>
358
359=item * L<http://openguides.org/>
360
361=item * L<http://chefmoz.org/>
362
363=back
364
365=head1 AUTHOR
366
367The OpenGuides Project (openguides-dev@openguides.org)
368
369=head1 COPYRIGHT
370
371Copyright (C) 2003-2005 The OpenGuides Project.  All Rights Reserved.
372
373This module is free software; you can redistribute it and/or modify it
374under the same terms as Perl itself.
375
376=head1 CREDITS
377
378Code in this module written by Kake Pugh and Earle Martin.  Dan Brickley, Matt
379Biddulph and other inhabitants of #swig on irc.freenode.net gave useful feedback
380and advice.
381
382=cut
383
3841;
Note: See TracBrowser for help on using the repository browser.