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

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

locales also needed escaping; plus fixed strange mixture of array and reference

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 12.3 KB
Line 
1package OpenGuides::RDF;
2
3use strict;
4
5use vars qw( $VERSION );
6$VERSION = '0.071';
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
54    $self;
55}
56
57sub emit_rdfxml {
58    my ($self, %args) = @_;
59
60    my $node_name = $args{node};
61    my $wiki = $self->{wiki};
62
63    my %node_data          = $wiki->retrieve_node( $node_name );
64    my $phone              = $node_data{metadata}{phone}[0]              || '';
65    my $fax                = $node_data{metadata}{fax}[0]                || '';
66    my $website            = $node_data{metadata}{website}[0]            || '';
67    my $opening_hours_text = $node_data{metadata}{opening_hours_text}[0] || '';
68    my $address            = $node_data{metadata}{address}[0]            || '';
69    my $postcode           = $node_data{metadata}{postcode}[0]           || '';
70    my $city               = $node_data{metadata}{city}[0]               || $self->{default_city};
71    my $country            = $node_data{metadata}{country}[0]            || $self->{default_country};
72    my $latitude           = $node_data{metadata}{latitude}[0]           || '';
73    my $longitude          = $node_data{metadata}{longitude}[0]          || '';
74    my $version            = $node_data{version};
75    my $username           = $node_data{metadata}{username}[0]           || '';
76    my $os_x               = $node_data{metadata}{os_x}[0]               || '';
77    my $os_y               = $node_data{metadata}{os_y}[0]               || '';
78    my @categories         = @{ $node_data{metadata}{category} || [] };
79    my @locales            = @{ $node_data{metadata}{locale} || [] };
80    my $summary            = $node_data{metadata}{summary}[0]            || '';
81
82    # replace any errant characters in data to prevent illegal XML
83    foreach ($phone, $fax, $website, $opening_hours_text, $address, $postcode, 
84             $city, $country, $latitude, $longitude, $version, $os_x, $os_y, 
85             @categories, @locales, $summary)
86    {
87      if ($_)
88      {
89        $_ =~ s/&/&/g;
90        $_ =~ s/</&lt;/g;
91        $_ =~ s/>/&gt;/g;
92      }
93    }
94   
95    my ($is_geospatial, $objType);
96
97    if ($os_x || $os_y || $latitude || $longitude || $address || $postcode || @locales) {
98        $is_geospatial = 1;
99        $objType    = 'geo:SpatialThing';
100    } else {
101        $objType = 'rdf:Description';
102    }
103
104    my $timestamp = $node_data{last_modified};
105   
106    # Make a Time::Piece object.
107    my $timestamp_fmt = $CGI::Wiki::Store::Database::timestamp_fmt;
108
109    if ( $timestamp ) {
110        my $time   = Time::Piece->strptime($timestamp, $timestamp_fmt);
111        $timestamp = $time->strftime("%Y-%m-%dT%H:%M:%S");
112    }
113
114    my $url               = $self->{make_node_url}->( $node_name, $version );
115    my $version_indpt_url = $self->{make_node_url}->( $node_name );
116
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_description    => $self->{site_description},
211          make_node_url       => $self->{make_node_url},
212          recent_changes_link => $self->{config}->script_url . uri_escape($self->{config}->script_name) . "?RecentChanges"
213        );
214    }
215   
216    $self->{rss_maker};
217}
218
219sub make_recentchanges_rss {
220    my ($self, %args) = @_;
221
222    $self->rss_maker->recent_changes(%args);
223}
224
225sub rss_timestamp {
226    my ($self, %args) = @_;
227   
228    $self->rss_maker->rss_timestamp(%args);
229}
230
231=head1 NAME
232
233OpenGuides::RDF - An OpenGuides plugin to output RDF/XML.
234
235=head1 DESCRIPTION
236
237Does all the RDF stuff for OpenGuides.  Distributed and installed as
238part of the OpenGuides project, not intended for independent
239installation.  This documentation is probably only useful to OpenGuides
240developers.
241
242=head1 SYNOPSIS
243
244    use CGI::Wiki;
245    use OpenGuides::Config;
246    use OpenGuides::RDF;
247
248    my $wiki = CGI::Wiki->new( ... );
249    my $config = OpenGuides::Config->new( file => "wiki.conf" );
250    my $rdf_writer = OpenGuides::RDF->new( wiki   => $wiki,
251                                         config => $config );
252
253    # RDF version of a node.
254    print "Content-Type: text/plain\n\n";
255    print $rdf_writer->emit_rdfxml( node => "Masala Zone, N1 0NU" );
256
257    # Ten most recent changes.
258    print "Content-Type: text/plain\n";
259    print "Last-Modified: " . $self->rss_timestamp( items => 10 ) . "\n\n";
260    print $rdf_writer->make_recentchanges_rss( items => 10 );
261
262=head1 METHODS
263
264=over 4
265
266=item B<new>
267
268    my $rdf_writer = OpenGuides::RDF->new( wiki   => $wiki,
269                                           config => $config );
270
271C<wiki> must be a L<CGI::Wiki> object and C<config> must be an
272L<OpenGuides::Config> object.  Both arguments mandatory.
273
274
275=item B<emit_rdfxml>
276
277    $wiki->write_node( "Masala Zone, N1 0NU",
278                     "Quick and tasty Indian food",
279                     $checksum,
280                     { comment  => "New page",
281                       username => "Kake",
282                       locale   => "Islington" }
283    );
284
285    print "Content-Type: text/plain\n\n";
286    print $rdf_writer->emit_rdfxml( node => "Masala Zone, N1 0NU" );
287
288B<Note:> Some of the fields emitted by the RDF/XML generator are taken
289from the node metadata. The form of this metadata is I<not> mandated
290by L<CGI::Wiki>. Your wiki application should make sure to store some or
291all of the following metadata when calling C<write_node>:
292
293=over 4
294
295=item B<postcode> - The postcode or zip code of the place discussed by the node.  Defaults to the empty string.
296
297=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.
298
299=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.
300
301=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.
302
303=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.
304
305=item B<phone> - Only one number supported at the moment.  No validation.
306
307=item B<website> - No validation.
308
309=item B<opening_hours_text> - A freeform text field.
310
311=back
312
313=item B<rss_maker>
314
315Returns a raw L<CGI::Wiki::Plugin::RSS::ModWiki> object created with the values you
316invoked this module with.
317
318=item B<make_recentchanges_rss>
319
320    # Ten most recent changes.
321    print "Content-Type: text/plain\n";
322    print "Last-Modified: " . $rdf_writer->rss_timestamp( items => 10 ) . "\n\n";
323    print $rdf_writer->make_recentchanges_rss( items => 10 );
324
325    # All the changes made by bob in the past week, ignoring minor edits.
326
327    my %args = (
328                 days               => 7,
329                 ignore_minor_edits => 1,
330                 filter_on_metadata => { username => "bob" },
331               );
332
333    print "Content-Type: text/plain\n";
334    print "Last-Modified: " . $rdf_writer->rss_timestamp( %args ) . "\n\n";
335    print $rdf_writer->make_recentchanges_rss( %args );
336
337=item B<rss_timestamp>
338
339    print "Last-Modified: " . $rdf_writer->rss_timestamp( %args ) . "\n\n";
340
341Returns the timestamp of the RSS feed in POSIX::strftime style ("Tue, 29 Feb 2000
34212:34:56 GMT"), which is equivalent to the timestamp of the most recent item
343in the feed. Takes the same arguments as make_recentchanges_rss(). You will most
344likely need this to print a Last-Modified HTTP header so user-agents can determine
345whether they need to reload the feed or not.
346
347=back
348
349=head1 SEE ALSO
350
351=over 4
352
353=item * L<CGI::Wiki>
354
355=item * L<http://openguides.org/>
356
357=item * L<http://chefmoz.org/>
358
359=back
360
361=head1 AUTHOR
362
363The OpenGuides Project (openguides-dev@openguides.org)
364
365=head1 COPYRIGHT
366
367Copyright (C) 2003-2005 The OpenGuides Project.  All Rights Reserved.
368
369This module is free software; you can redistribute it and/or modify it
370under the same terms as Perl itself.
371
372=head1 CREDITS
373
374Code in this module written by Kake Pugh and Earle Martin.  Dan Brickley, Matt
375Biddulph and other inhabitants of #swig on irc.freenode.net gave useful feedback
376and advice.
377
378=cut
379
3801;
Note: See TracBrowser for help on using the repository browser.