File Coverage

blib/lib/XML/GRDDL.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package XML::GRDDL;
2              
3 1     1   51913 use 5.008;
  1         6  
  1         67  
4 1     1   9 use strict;
  1         2  
  1         60  
5 1     1   9 use constant GRDDL_NS => 'http://www.w3.org/2003/g/data-view#';
  1         10  
  1         145  
6 1     1   9 use constant XHTML_NS => 'http://www.w3.org/1999/xhtml';
  1         2  
  1         82  
7              
8 1     1   7 use Carp;
  1         3  
  1         150  
9 1     1   1354 use Data::UUID;
  1         35397  
  1         154  
10 1     1   655 use RDF::RDFa::Parser '1.097';
  0            
  0            
11             use RDF::Trine qw[iri statement];
12             use Scalar::Util qw[blessed];
13             use URI;
14             use URI::Escape qw[uri_escape];
15             use XML::GRDDL::Namespace;
16             use XML::GRDDL::Profile;
17             use XML::GRDDL::Transformation;
18             use XML::LibXML;
19              
20             our $VERSION = '0.004';
21              
22             use base 'Exporter';
23             our @EXPORT_OK = qw( GRDDL_NS XHTML_NS );
24              
25             sub new
26             {
27             my ($class) = @_;
28             return bless { cache=>{}, ua=>undef, }, $class;
29             }
30              
31             sub ua
32             {
33             my $self = shift;
34             if (@_)
35             {
36             my $rv = $self->{'ua'};
37             $self->{'ua'} = shift;
38             croak "Set UA to something that is not an LWP::UserAgent!"
39             unless blessed $self->{'ua'} && $self->{'ua'}->isa('LWP::UserAgent');
40             return $rv;
41             }
42             unless (blessed $self->{'ua'} && $self->{'ua'}->isa('LWP::UserAgent'))
43             {
44             $self->{'ua'} = LWP::UserAgent->new(agent=>sprintf('%s/%s ', __PACKAGE__, $VERSION));
45             }
46             return $self->{'ua'};
47             }
48              
49             sub data
50             {
51             my ($self, $document, $uri, %options) = @_;
52            
53             unless (blessed($document) && $document->isa('XML::LibXML::Document'))
54             {
55             my $parser = XML::LibXML->new;
56             $document = $parser->parse_string($document);
57             }
58            
59             my $model = RDF::Trine::Model->temporary_model;
60             my @transformations = $self->discover($document, $uri, %options, strings => 0);
61              
62             foreach my $t (@transformations)
63             {
64             my $m = $t->model($document);
65             if ($m)
66             {
67             my $context = iri('urn:uuid:'.Data::UUID->new->create_str);
68             my $rootnode = iri('urn:uuid:'.Data::UUID->new->create_str);
69             my $property = iri('http://ontologi.es/grddl?transformation='.uri_escape($t->uri).'#result');
70             $model->add_hashref($m->as_hashref, $context);
71            
72             if ($options{metadata})
73             {
74             $model->add_statement(statement(
75             iri($uri),
76             iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
77             iri(GRDDL_NS.'InformationResource'),
78             ));
79             $model->add_statement(statement(
80             iri($uri),
81             iri(GRDDL_NS.'rootNode'),
82             $rootnode,
83             ));
84             $model->add_statement(statement(
85             $rootnode,
86             iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
87             iri(GRDDL_NS.'RootNode'),
88             ));
89             $model->add_statement(statement(
90             iri($uri),
91             iri(GRDDL_NS.'result'),
92             $context,
93             ));
94             $model->add_statement(statement(
95             $context,
96             iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
97             iri(GRDDL_NS.'RDFGraph'),
98             ));
99             $model->add_statement(statement(
100             iri($t->uri),
101             iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
102             iri(GRDDL_NS.'Transformation'),
103             ));
104             $model->add_statement(statement(
105             iri($t->uri),
106             iri(GRDDL_NS.'transformationProperty'),
107             $property,
108             ));
109             $model->add_statement(statement(
110             $property,
111             iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
112             iri(GRDDL_NS.'TransformationProperty'),
113             ));
114             $model->add_statement(statement(
115             $rootnode,
116             $property,
117             $context,
118             ));
119             }
120             }
121             }
122            
123             return $model;
124             }
125              
126             sub discover
127             {
128             my ($self, $document, $uri, %options) = @_;
129              
130             unless (blessed($document) && $document->isa('XML::LibXML::Document'))
131             {
132             my $parser = XML::LibXML->new;
133             $document = $parser->parse_string($document);
134             }
135            
136             my @transformations;
137              
138             push @transformations,
139             $self->_discover_from_rel_attribute($document, $uri, %options);
140              
141             push @transformations,
142             $self->_discover_from_transformation_attribute($document, $uri, %options);
143              
144             push @transformations,
145             $self->_discover_from_profiles($document, $uri, %options);
146            
147             push @transformations,
148             $self->_discover_from_namespace($document, $uri, %options);
149              
150             if ($options{'strings'})
151             {
152             return @transformations;
153             }
154             else
155             {
156             return map { XML::GRDDL::Transformation->new($_, $uri, $self); } @transformations;
157             }
158             }
159              
160             sub _discover_from_rel_attribute
161             {
162             my ($self, $document, $uri, %options) = @_;
163             my @transformations;
164              
165             my $profile_found = $options{'force_rel'};
166            
167             my $xpc = XML::LibXML::XPathContext->new;
168             $xpc->registerNs(xhtml => XHTML_NS);
169            
170             unless ($profile_found)
171             {
172             my @nodes = $xpc->findnodes('/xhtml:html/xhtml:head[@profile]', $document);
173             foreach my $head (@nodes)
174             {
175             if ($head->getAttribute('profile') =~ m!(^|\s) http://www\.w3\.org/2003/g/data-view\#? (\s|$)!x)
176             {
177             $profile_found = 1;
178             last;
179             }
180             }
181             }
182            
183             if ($profile_found)
184             {
185             my $is_html = $document->documentElement->namespaceURI eq XHTML_NS;
186             my $rdfa = $self->_rdf_model($document, $uri, $is_html?'application/xhtml+xml':'application/xml');
187             my $iter = $rdfa->get_statements(iri($uri), iri(GRDDL_NS.'transformation'), undef);
188             while (my $st = $iter->next)
189             {
190             next unless $st->object->is_resource;
191             push @transformations, $st->object->uri;
192             }
193             }
194            
195             return @transformations;
196             }
197              
198             sub _discover_from_transformation_attribute
199             {
200             my ($self, $document, $uri, %options) = @_;
201             my @transformations;
202              
203             # Right now just doing this on root element. Supposed to also check others??
204             my $attr = $document->documentElement->getAttributeNS(GRDDL_NS, 'transformation');
205             my @t = split /\s+/, $attr;
206             foreach my $t (@t)
207             {
208             next unless $t =~ /[a-z0-9\.]/i;
209             push @transformations, $self->_resolve_relative_ref($t, $uri);
210             }
211            
212             return @transformations;
213             }
214              
215             sub _discover_from_profiles
216             {
217             my ($self, $document, $uri, %options) = @_;
218             my @transformations;
219            
220             my $xpc = XML::LibXML::XPathContext->new;
221             $xpc->registerNs(xhtml => XHTML_NS);
222            
223             my @profiles;
224             my @nodes = $xpc->findnodes('/xhtml:html/xhtml:head[@profile]', $document);
225             foreach my $head (@nodes)
226             {
227             my @t = split /\s+/, $head->getAttribute('profile');
228             foreach my $t (@t)
229             {
230             next unless $t =~ /[a-z0-9\.]/i;
231             push @profiles, $self->_resolve_relative_ref($t, $uri);
232             }
233             }
234              
235             foreach my $profile (@profiles)
236             {
237             my $profile_object = XML::GRDDL::Profile->new($profile, $uri, $self);
238             push @transformations, $profile_object->transformations;
239             }
240              
241             return @transformations;
242             }
243              
244             sub _discover_from_namespace
245             {
246             my ($self, $document, $uri, %options) = @_;
247            
248             my $ns = $document->documentElement->namespaceURI;
249             my $ns_obj = XML::GRDDL::Namespace->new($ns, $uri, $self);
250            
251             return $ns_obj->transformations;
252             }
253              
254             sub _fetch
255             {
256             my ($self, $document, %headers) = @_;
257             $self->{'cache'}->{$document} ||= $self->ua->get($document, %headers);
258             return $self->{'cache'}->{$document};
259             }
260              
261             sub _rdf_model
262             {
263             my ($self, $document, $uri, $type, $nocache) = @_;
264            
265             if ($nocache || !$self->{'cached-rdf'}->{$uri})
266             {
267             if ($type eq 'application/xhtml+xml'
268             or $type eq 'text/html'
269             or $type eq 'application/atom+xml'
270             or $type eq 'image/svg+xml')
271             {
272             my $config = RDF::RDFa::Parser::Config->new(
273             $type,
274             '1.1',
275             initial_context => 'http://www.w3.org/2003/g/data-view',
276             );
277             my $parser = RDF::RDFa::Parser->new($document, $uri, $config);
278             return $parser->graph if $nocache;
279             $self->{'cached-rdf'}->{$uri} = $parser->graph;
280             }
281             else
282             {
283             if (blessed($document))
284             {
285             $document = $document->toString;
286             }
287             my $model = RDF::Trine::Model->temporary_model;
288             my $pclass = $RDF::Trine::Parser::media_types{ $type };
289             my $parser = ($pclass && $pclass->can('new'))
290             ? $pclass->new
291             : RDF::Trine::Parser::RDFXML->new;
292             $parser->parse_into_model($uri, $document, $model);
293             return $model if $nocache;
294             $self->{'cached-rdf'}->{$uri} = $model;
295             }
296             }
297            
298             return $self->{'cached-rdf'}->{$uri};
299             }
300              
301             sub _resolve_relative_ref
302             {
303             my ($self, $ref, $base) = @_;
304              
305             return $ref unless $base; # keep relative unless we have a base URI
306              
307             if ($ref =~ /^([a-z][a-z0-9\+\.\-]*)\:/i)
308             {
309             return $ref; # already an absolute reference
310             }
311              
312             # create absolute URI
313             my $abs = URI->new_abs($ref, $base)->canonical->as_string;
314              
315             while ($abs =~ m!^(http://.*)(\.\./|\.)+(\.\.|\.)?$!i)
316             { $abs = $1; } # fix edge case of 'http://example.com/../../../'
317              
318             return $abs;
319             }
320              
321             1;
322              
323             __END__
324              
325             =head1 NAME
326              
327             XML::GRDDL - transform XML and XHTML to RDF
328              
329             =head1 SYNOPSIS
330              
331             High-level interface:
332              
333             my $grddl = XML::GRDDL->new;
334             my $model = $grddl->data($xmldoc, $baseuri);
335             # $model is an RDF::Trine::Model
336              
337             Low-level interface:
338              
339             my $grddl = XML::GRDDL->new;
340             my @transformations = $grddl->discover($xmldoc, $baseuri);
341             foreach my $t (@transformations)
342             {
343             # $t is an XML::GRDDL::Transformation
344             my ($output, $mediatype) = $t->transform($xmldoc);
345             # $output is a string of type $mediatype.
346             }
347              
348             =head1 DESCRIPTION
349              
350             GRDDL is a W3C Recommendation for extracting RDF data from arbitrary
351             XML and XHTML via a transformation, typically written in XSLT. See
352             L<http://www.w3.org/TR/grddl/> for more details.
353              
354             This module implements GRDDL in Perl. It offers both a low level interface,
355             allowing you to generate a list of transformations associated with the
356             document being processed, and thus the ability to selectively run the
357             transformation; and a high-level interface where a single RDF model
358             is returned representing the union of the RDF graphs generated by
359             applying all available transformations.
360              
361             =head2 Constructor
362              
363             =over 4
364              
365             =item C<< XML::GRDDL->new >>
366              
367             The constructor accepts no parameters and returns an XML::GRDDL
368             object.
369              
370             =back
371              
372             =head2 Methods
373              
374             =over 4
375              
376             =item C<< $grddl->discover($xml, $base, %options) >>
377              
378             Processes the document to discover the transformations associated
379             with it. $xml is the raw XML source of the document, or an
380             XML::LibXML::Document object. ($xml cannot be "tag soup" HTML,
381             though you should be able to use L<HTML::HTML5::Parser> to
382             parse tag soup into an XML::LibXML::Document.) $base is the
383             base URI for resolving relative references.
384              
385             Returns a list of L<XML::GRDDL::Transformation> objects.
386              
387             Options include:
388              
389             =over 4
390              
391             =item * B<force_rel> - boolean; interpret XHTML rel="transformation" even in the absence of the GRDDL profile.
392              
393             =item * B<strings> - boolean; return a list of plain strings instead of blessed objects.
394              
395             =back
396              
397             =item C<< $grddl->data($xml, $base, %options) >>
398              
399             Processes the document, discovers the transformations associated
400             with it, applies the transformations and merges the results into a
401             single RDF model. $xml and $base are as per C<discover>.
402              
403             Returns an RDF::Trine::Model containing the data. Statement contexts
404             (a.k.a. named graphs / quads) are used to distinguish between data
405             from the result of each transformation.
406              
407             Options include:
408              
409             =over 4
410              
411             =item * B<force_rel> - boolean; interpret XHTML rel="transformation" even in the absence of the GRDDL profile.
412              
413             =item * B<metadata> - boolean; include provenance information in the default graph (a.k.a. nil context).
414              
415             =back
416              
417             =item C<< $grddl->ua( [$ua] ) >>
418              
419             Get/set the user agent used for HTTP requests. $ua, if supplied, must be
420             an LWP::UserAgent.
421              
422             =back
423              
424             =head2 Constants
425              
426             These constants may be exported upon request.
427              
428             =over
429              
430             =item C<< GRDDL_NS >>
431              
432             =item C<< XHTML_NS >>
433              
434             =back
435              
436             =head1 FEATURES
437              
438             XML::GRDDL supports transformations written in XSLT 1.0, and in RDF-EASE.
439              
440             XML::GRDDL is a good HTTP citizen: Referer headers are included in requests,
441             and appropriate Accept headers supplied. To be an even better citizen, I
442             recommend changing the User-Agent header to advertise the name of the
443             application:
444              
445             $grddl->ua->default_header(user_agent => 'MyApp/1.0 ');
446              
447             Provenance information for GRDDL transformations is returned using the
448             GRDDL vocabulary at L<http://www.w3.org/2003/g/data-view#>.
449              
450             Certain XHTML profiles and XML namespaces known not to contain any
451             transformations, or to contain useless transformations are skipped. See
452             L<XML::GRDDL::Namespace> and L<XML::GRDDL::Profile> for details. In
453             particular profiles for RDFa and many Microformats are skipped, as
454             L<RDF::RDFa::Parser> and L<HTML::Microformats> will typically yield
455             far superior results.
456              
457             =head1 BUGS
458              
459             Please report any bugs to L<http://rt.cpan.org/>.
460              
461             Known limitations:
462              
463             =over 4
464              
465             =item * Recursive GRDDL doesn't work yet.
466              
467             That is, the profile documents and namespace documents linked to from
468             your primary document cannot themselves rely on GRDDL.
469              
470             =back
471              
472             =head1 SEE ALSO
473              
474             L<XML::GRDDL::Transformation>,
475             L<XML::GRDDL::Namespace>,
476             L<XML::GRDDL::Profile>,
477             L<XML::GRDDL::Transformation::RDF_EASE::Functional>,
478             L<XML::Saxon::XSLT2>.
479              
480             L<HTML::HTML5::Parser>,
481             L<RDF::RDFa::Parser>,
482             L<HTML::Microformats>.
483              
484             L<JSON::GRDDL>.
485              
486             L<http://www.w3.org/TR/grddl/>.
487              
488             L<http://www.perlrdf.org/>.
489              
490             This module is derived from Swignition L<http://buzzword.org.uk/swignition/>.
491              
492             =head1 AUTHOR
493              
494             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
495              
496             =head1 COPYRIGHT AND LICENCE
497              
498             Copyright 2008-2012 Toby Inkster
499              
500             This library is free software; you can redistribute it and/or modify it
501             under the same terms as Perl itself.
502              
503             =head1 DISCLAIMER OF WARRANTIES
504              
505             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
506             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
507             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.