File Coverage

blib/lib/RDF/RDB2RDF/R2RML.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package RDF::RDB2RDF::R2RML;
2              
3 1     1   25631 use 5.010;
  1         4  
  1         40  
4 1     1   5 use strict;
  1         1  
  1         46  
5 1     1   1117 use utf8;
  1         11  
  1         7  
6              
7 1     1   46 use Digest::MD5 qw[md5_hex];
  1         2  
  1         81  
8 1     1   493 use RDF::Trine qw[statement blank literal];
  0            
  0            
9             use RDF::Trine::Namespace qw[rdf rdfs owl xsd];
10             use Scalar::Util qw[blessed];
11             use Storable qw[dclone];
12              
13             our $rr = RDF::Trine::Namespace->new('http://www.w3.org/ns/r2rml#');
14             our $rrx = RDF::Trine::Namespace->new('http://purl.org/r2rml-ext/');
15              
16             use namespace::clean;
17             use base qw[
18             RDF::RDB2RDF::Simple
19             ];
20              
21             our $AUTHORITY = 'cpan:TOBYINK';
22             our $VERSION = '0.008';
23              
24             sub _COL_
25             {
26             package
27             RDF::RDB2RDF::R2RML::_COL_;
28             use overload fallback => 1, q[""] => sub { ${+shift} };
29             bless \$_[0];
30             }
31              
32             sub new
33             {
34             my ($class, $r2rml) = @_;
35             my $self = $class->SUPER::new();
36             $self->_r2rml($r2rml);
37             return $self;
38             }
39              
40             sub process_turtle
41             {
42             my ($self, $dbh, %options) = @_;
43             my $rv = $self->SUPER::process_turtle($dbh, %options);
44            
45             unless ($options{no_r2rml})
46             {
47             my $r2rml = RDF::Trine::Serializer
48             ->new('Turtle', namespaces => { $self->namespaces })
49             ->serialize_model_to_string($self->{r2rml});
50             $r2rml =~ s/^/# /gm;
51             $rv = "# R2RML\n#\n${r2rml}\n${rv}";
52             }
53             }
54              
55             sub _r2rml
56             {
57             my ($self, $r2rml) = @_;
58            
59             unless (blessed($r2rml) and $r2rml->isa('RDF::Trine::Model'))
60             {
61             $self->{namespaces} = RDF::Trine::NamespaceMap->new;
62             my $parser = RDF::Trine::Parser->new('Turtle', namespaces=>$self->{namespaces});
63             my $model = RDF::Trine::Model->temporary_model;
64             $parser->parse_into_model('http://example.com/', $r2rml, $model);
65             $r2rml = $model;
66             }
67            
68             my @TMC = values %{ {
69             map { $_->as_ntriples => $_ }
70             (
71             $r2rml->subjects($rdf->type, $rr->TriplesMap),
72             $r2rml->subjects($rdf->type, $rr->TriplesMapClass),
73             $r2rml->subjects($rr->subjectMap, undef),
74             )
75             } };
76            
77             foreach my $tmc (@TMC)
78             {
79             $self->_r2rml_TriplesMap($r2rml, $tmc);
80             }
81            
82             $self->{r2rml} = $r2rml;
83             }
84              
85             sub _r2rml_TriplesMap
86             {
87             my ($self, $r2rml, $tmc) = @_;
88             my $mapping = {};
89            
90             if ( $self->{tmc}{$tmc} )
91             {
92             return $self->{tmc}{$tmc};
93             }
94            
95             my ($tablename, $sqlquery);
96             foreach ($r2rml->objects_for_predicate_list($tmc, $rr->SQLQuery, $rr->sqlQuery))
97             {
98             next unless $_->is_literal;
99             $sqlquery = $_->literal_value;
100             last;
101             }
102             if ($sqlquery)
103             {
104             $tablename = sprintf('+q%s', md5_hex($sqlquery));
105             $mapping->{sql} = $sqlquery;
106             }
107             else
108             {
109             foreach ($r2rml->objects($tmc, $rr->tableName))
110             {
111             next unless $_->is_literal;
112             $tablename = $_->literal_value;
113             last;
114             }
115             if ($tablename)
116             {
117             foreach ($r2rml->objects($tmc, $rr->tableOwner))
118             {
119             next unless $_->is_literal;
120             $tablename = sprintf('%s.%s', $_->literal_value, $tablename);
121             last;
122             }
123             }
124             }
125            
126             unless ($tablename)
127             {
128             LOGICALTABLE: foreach my $lt ($r2rml->objects($tmc, $rr->logicalTable))
129             {
130             next LOGICALTABLE if $lt->is_literal;
131              
132             foreach ($r2rml->objects_for_predicate_list($lt, $rr->sqlQuery, $rr->SQLQuery))
133             {
134             next unless $_->is_literal;
135             $sqlquery = $_->literal_value;
136             last;
137             }
138             if ($sqlquery)
139             {
140             $tablename = sprintf('+q%s', md5_hex($sqlquery));
141             $mapping->{sql} = $sqlquery;
142             last LOGICALTABLE;
143             }
144              
145             TABLENAME: foreach ($r2rml->objects($lt, $rr->tableName))
146             {
147             next TABLENAME unless $_->is_literal;
148             $tablename = $_->literal_value;
149             last TABLENAME;
150             }
151             if ($tablename)
152             {
153             TABLEOWNER: foreach ($r2rml->objects($tmc, $rr->tableOwner))
154             {
155             next TABLEOWNER unless $_->is_literal;
156             $tablename = sprintf('%s.%s', $_->literal_value, $tablename);
157             last TABLEOWNER ;
158             }
159             last LOGICALTABLE;
160             }
161             }
162             }
163            
164             return unless $tablename;
165             $self->{tmc}{$tmc} = $mapping;
166             $mapping->{from} = $tablename unless defined $mapping->{sql};
167            
168             foreach ($r2rml->objects($tmc, $rr->subjectMap))
169             {
170             next if $_->is_literal;
171             $self->_r2rml_SubjectMap($r2rml, $_, $mapping);
172             last;
173             }
174            
175             unless ($mapping->{about})
176             {
177             ($mapping->{about}) = grep { !$_->is_literal } $r2rml->objects_for_predicate_list($tmc, $rr->subject);
178             }
179              
180             foreach ($r2rml->objects($tmc, $rr->predicateObjectMap))
181             {
182             next if $_->is_literal;
183             $self->_r2rml_PredicateObjectMap($r2rml, $_, $mapping);
184             }
185              
186             my $key = $tablename;
187             while (defined $self->{mappings}{$key})
188             {
189             $key = sprintf('+t%s', md5_hex($key));
190             }
191            
192             $self->{mappings}{$key} = $mapping;
193            
194             return $mapping;
195             }
196              
197             sub _r2rml_graph
198             {
199             my ($self, $r2rml, $thing) = @_;
200            
201             my ($graph) =
202             map { $_->is_resource ? $_->uri : $_->as_ntriples }
203             grep { !$_->is_literal }
204             $r2rml->objects($thing, $rr->graph);
205             return $graph if $graph;
206              
207             foreach my $map ($r2rml->objects($thing, $rr->graphMap))
208             {
209             ($graph) =
210             map { $_->is_resource ? $_->uri : $_->as_ntriples }
211             grep { !$_->is_literal }
212             $r2rml->objects_for_predicate_list($thing, $rr->constant, $rr->graph);
213             return $graph if $graph;
214            
215             ($graph) =
216             map { sprintf('{%s}', $_->literal_value) }
217             grep { $_->is_literal }
218             $r2rml->objects($thing, $rr->column);
219             return _COL_ $graph if $graph;
220              
221             ($graph) =
222             map { $_->literal_value }
223             grep { $_->is_literal }
224             $r2rml->objects($thing, $rr->template);
225             return $graph if $graph;
226             }
227            
228             return;
229             }
230              
231             sub _r2rml_SubjectMap
232             {
233             my ($self, $r2rml, $smc, $mapping) = @_;
234            
235             # the easy bit
236             $mapping->{typeof} = [ grep { !$_->is_literal } $r2rml->objects($smc, $rr->class) ];
237            
238             # graph
239             $mapping->{graph} = $self->_r2rml_graph($r2rml, $smc);
240              
241             # subject
242             ($mapping->{about}) =
243             map { $_->is_resource ? $_->uri : $_->as_ntriples }
244             grep { !$_->is_literal }
245             $r2rml->objects_for_predicate_list($smc, $rr->constant, $rr->subject);
246             unless ($mapping->{about})
247             {
248             my ($col) = grep { $_->is_literal } $r2rml->objects($smc, $rr->column);
249             $mapping->{about} = _COL_ sprintf('{%s}', $col->literal_value) if $col;
250             $mapping->{_about_is_column} = 1 if $col;
251             }
252             unless ($mapping->{about})
253             {
254             my ($tmpl) = grep { $_->is_literal } $r2rml->objects($smc, $rr->template);
255             $mapping->{about} = $tmpl->literal_value if $tmpl;
256             $mapping->{_about_is_template} = 1 if $tmpl;
257             }
258            
259             # termtype
260             my ($termtype) =
261             map {
262             if ($_->as_ntriples =~ /(uri|iri|blank|blanknode|literal).?$/i)
263             { { uri=>'IRI', iri=>'IRI', blank=>'BlankNode', blanknode=>'BlankNode', literal=>'Literal' }->{lc $1} }
264             else
265             { $_->as_ntriples }
266             }
267             $r2rml->objects_for_predicate_list($smc, $rr->termType, $rr->termtype);
268             $termtype //= '';
269            
270             if ($mapping->{about} and $termtype =~ /^blank/i)
271             {
272             $mapping->{about} = sprintf('_:%s', $mapping->{about})
273             unless $mapping->{about} =~ /^_:/;
274             }
275             }
276              
277             sub _r2rml_PredicateObjectMap
278             {
279             my ($self, $r2rml, $pomc, $mapping) = @_;
280            
281             # graph
282             my $graph = $self->_r2rml_graph($r2rml, $pomc);
283              
284             # predicates
285             my @predicates;
286             foreach ($r2rml->objects($pomc, $rr->predicateMap))
287             {
288             next if $_->is_literal;
289             push @predicates, $self->_r2rml_PredicateMap($r2rml, $_);
290             }
291              
292             push @predicates,
293             map { $_->uri }
294             grep { $_->is_resource }
295             $r2rml->objects_for_predicate_list($pomc, $rr->predicate);
296              
297             # objects
298             my @objects;
299             foreach ($r2rml->objects($pomc, $rr->objectMap))
300             {
301             next if $_->is_literal;
302             my $obj = $self->_r2rml_ObjectMap($r2rml, $_);
303             push @objects, $obj if defined $obj;
304             }
305              
306             push @objects,
307             map {
308             my $x = {};
309             if ($_->is_literal)
310             {
311             $x->{content} = $_->literal_value;
312             $x->{lang} = $_->literal_value_language;
313             $x->{datatype} = $_->literal_datatype;
314             $x->{kind} = 'property';
315             }
316             elsif ($_->is_resource)
317             {
318             $x->{resource} = $_->uri;
319             $x->{kind} = 'rel';
320             }
321             elsif ($_->is_blank)
322             {
323             $x->{resource} = $_->as_ntriples;
324             $x->{kind} = 'rel';
325             }
326             $x;
327             }
328             $r2rml->objects_for_predicate_list($pomc, $rr->object);
329              
330             foreach ($r2rml->objects($pomc, $rr->refObjectMap))
331             {
332             next if $_->is_literal;
333             my $obj = $self->_r2rml_RefObjectMap($r2rml, $_);
334             push @objects, $obj if defined $obj;
335             }
336              
337             foreach my $obj (@objects)
338             {
339             foreach my $p (@predicates)
340             {
341             my $o = dclone($obj);
342             my $column = delete $o->{column} || '_';
343             my $kind = delete $o->{kind} || 'property';
344             $o->{$kind} = $p;
345            
346             push @{ $mapping->{columns}{$column} }, $o;
347             }
348             }
349             }
350              
351             sub _r2rml_PredicateMap
352             {
353             my ($self, $r2rml, $pmc) = @_;
354            
355             my ($p) = map { $_->uri } grep { $_->is_resource } $r2rml->objects_for_predicate_list($pmc, $rr->constant, $rr->predicate);
356             unless ($p)
357             {
358             my ($col) = grep { $_->is_literal } $r2rml->objects($pmc, $rr->column);
359             $p = _COL_ sprintf('{%s}', $col->literal_value) if $col;
360             }
361             unless ($p)
362             {
363             my ($tmpl) = grep { $_->is_literal } $r2rml->objects($pmc, $rr->template);
364             $p = $tmpl->literal_value if $tmpl;
365             }
366              
367             return ($p);
368             }
369              
370             sub _r2rml_ObjectMap
371             {
372             my ($self, $r2rml, $omc) = @_;
373            
374             my ($datatype, $lang_col, $language, $termtype, $column);
375             my ($o) = map {
376             if ($_->is_resource) { $termtype = 'IRI'; $_->value; }
377             elsif ($_->is_blank) { $termtype = 'BlankNode'; $_->as_ntriples; }
378             elsif ($_->is_literal) { $datatype = $_->literal_datatype; $language = $_->literal_value_language; $termtype = 'Literal'; $_->literal_value; }
379             else { $_->as_ntriples; }
380             }
381             $r2rml->objects_for_predicate_list($omc, $rr->constant, $rr->object);
382             unless (defined $o)
383             {
384             my ($col) = grep { $_->is_literal } $r2rml->objects($omc, $rr->column);
385             $o = _COL_ sprintf('{%s}', $col->literal_value) if $col;
386             $column = $col->literal_value if $col;
387             }
388             unless (defined $o)
389             {
390             my ($tmpl) = grep { $_->is_literal } $r2rml->objects($omc, $rr->template);
391             $o = $tmpl->literal_value if $tmpl;
392             }
393              
394             ($datatype) =
395             map { $_->uri }
396             grep { $_->is_resource }
397             $r2rml->objects($omc, $rr->datatype)
398             unless $datatype;
399             ($language) =
400             map { $_->literal_value }
401             grep { $_->is_literal }
402             $r2rml->objects($omc, $rr->language)
403             unless $language;
404             ($lang_col) =
405             map { $_->literal_value }
406             grep { $_->is_literal }
407             $r2rml->objects($omc, $rrx->languageColumn);
408             ($termtype) =
409             map {
410             if ($_->as_ntriples =~ /(uri|iri|blank|blanknode|literal).?$/i)
411             { { uri=>'IRI', iri=>'IRI', blank=>'BlankNode', blanknode=>'BlankNode', literal=>'Literal' }->{lc $1} }
412             else
413             { $_->as_ntriples }
414             }
415             $r2rml->objects_for_predicate_list($omc, $rr->termType, $rr->termtype)
416             unless $termtype;
417            
418             $termtype ||= 'Literal' if $datatype || $language || $lang_col || defined $column;
419             $termtype ||= 'IRI';
420            
421             $o = sprintf('_:%s', $o)
422             if (!ref $o) && $termtype =~ /^blank/i && $o !~ /^_:/;
423            
424             my $map = {};
425            
426             if ($column)
427             {
428             #$column = $1 if $column =~ m{^"(.+)"$};
429             $map->{column} = $column;
430             }
431             else
432             {
433             my $x = ($termtype =~ /literal/i) ? 'content' : 'resource';
434             $map->{$x} = $o;
435             }
436            
437             $map->{datatype} = $datatype if $datatype;
438             $map->{lang} = $language if $language;
439             $map->{kind} = ($termtype =~ /literal/i) ? 'property' : 'rel';
440             $map->{lang_col} = $lang_col if $lang_col;
441              
442             return $map;
443             }
444              
445             sub _r2rml_RefObjectMap
446             {
447             my ($self, $r2rml, $romc) = @_;
448            
449             my $parent;
450             PARENT: foreach my $ptm ($r2rml->objects($romc, $rr->parentTriplesMap))
451             {
452             next PARENT if $ptm->is_literal;
453             $parent = $self->_r2rml_TriplesMap($r2rml, $ptm);
454             last PARENT if $parent;
455             }
456             return unless $parent;
457            
458             my $joins = [];
459             JOIN: foreach my $jc ($r2rml->objects($romc, $rr->joinCondition))
460             {
461             my ($p) = grep { $_->is_literal }
462             $r2rml->objects($jc, $rr->parent);
463             my ($c) = grep { $_->is_literal }
464             $r2rml->objects($jc, $rr->child);
465            
466             if ($p && $c)
467             {
468             push @$joins, { parent => $p->literal_value, child => $c->literal_value };
469             }
470             }
471            
472             return {
473             column => '_',
474             join => $parent->{sql} || $parent->{from},
475             on => $joins,
476             resource => $parent->{about},
477             method => $parent->{sql} ? 'subquery' : 'table',
478             };
479             }
480              
481             1;
482              
483             __END__
484              
485             =encoding utf8
486              
487             =head1 NAME
488              
489             RDF::RDB2RDF::R2RML - map relational database to RDF using R2RML
490              
491             =head1 SYNOPSIS
492              
493             my $mapper = RDF::RDB2RDF->new('R2RML', $r2rml);
494             print $mapper->process_turtle($dbh);
495              
496             =head1 DESCRIPTION
497              
498             This class offers support for W3C R2RML, based on the 29 May 2012 working
499             draft. See the COMPLIANCE AND COMPATIBILITY section below for a list on
500             unimplemented areas.
501              
502             This is a subclass of RDF::RDB2RDF::Simple. Differences noted below...
503              
504             =head2 Constructor
505              
506             =over
507              
508             =item * C<< RDF::RDB2RDF::R2RML->new($r2rml) >>
509              
510             =item * C<< RDF::RDB2RDF->new('R2RML', $r2rml) >>
511              
512             A single parameter is expected, this can either be an R2RML document as a
513             Turtle string, or an L<RDF::Trine::Model> containing R2RML data. If a Turtle
514             string, then the namespaces from it are also kept.
515              
516             =back
517              
518             =head2 Methods
519              
520             =over
521              
522             =item * C<< process_turtle($dbh, %options) >>
523              
524             The mapping is included as an R2RML comment at the top of the Turtle. Passing
525             C<< no_r2rml => 1 >> can disable that feature.
526              
527             =back
528              
529              
530             =head1 COMPLIANCE AND COMPATIBILITY
531              
532             This implementation should be mostly compliant with the Direct Mapping
533             specification, with the following provisos:
534              
535             =over
536              
537             =item * rr:RefObjectMap, rr:parentTriplesMap, rr:joinCondition,
538             rr:JoinCondition, rr:child, rr:parent are only partially working.
539              
540             =item * rr:defaultGraph is not understood.
541              
542             =back
543              
544             Other quirks are database-specific:
545              
546             =over
547              
548             =item * This module expects DBI to return utf8 character strings. Depending
549             on your database engine, you might need to play games with DBI and
550             your database server to get this working. If you're using 7-bit safe
551             ASCII, then this probably doesn't concern you.
552              
553             =item * Different databases support different SQL datatypes. This module
554             attempts to map them to their XSD equivalents, but may not recognise
555             some exotic ones.
556              
557             =item * This module has only been extensively tested on SQLite 3.6.23.1
558             and PostgreSQL 8.4.4. I know of no reason it shouldn't work with other
559             relational database engines, provided they are supported by DBI, but as
560             with all things SQL, I wouldn't be surprised if there were one or two
561             problems. Patches welcome.
562              
563             =back
564              
565             =head2 Language Extension
566              
567             rr:language allows you to assign only a constant language tag. This module
568             implements an extension in case you need to assign the language dynamically
569             (from a table or view column). It's defined as a property rrx:languageColumn:
570              
571             @prefix rr: <http://www.w3.org/ns/r2rml#>.
572             @prefix rrx: <http://purl.org/r2rml-ext/>.
573             @prefix bibo: <http://purl.org/ontology/bibo/>.
574             @prefix dc: <http://purl.org/dc/elements/1.1/>.
575            
576             []
577             rr:logicalTable [
578             rr:tableName "books";
579             ];
580             rr:subjectMap [
581             rr:class bibo:Book;
582             rr:template "book/{book_id}";
583             ];
584             rr:predicateObjectMap [
585             rr:predicate dc:title;
586             rr:objectMap [
587             rr:column "title";
588             rrx:languageColumn "title_lang";
589             rr:language "en"; # fallback for nulls
590             ];
591             ].
592              
593             Please note this must be a valid IANA language tag.
594              
595             =head1 BUGS
596              
597             Please report any bugs to
598             L<http://rt.cpan.org/Dist/Display.html?Queue=RDF-RDB2RDF>.
599              
600             =head1 SEE ALSO
601              
602             L<RDF::Trine>, L<RDF::RDB2RDF>, L<RDF::RDB2RDF::Simple>.
603              
604             L<http://www.perlrdf.org/>.
605              
606             L<http://www.w3.org/TR/2012/WD-r2rml-20120529/>.
607              
608             =head1 AUTHOR
609              
610             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
611              
612             =head1 COPYRIGHT
613              
614             Copyright 2011-2013 Toby Inkster
615              
616             This library is free software; you can redistribute it and/or modify it
617             under the same terms as Perl itself.
618              
619             =head1 DISCLAIMER OF WARRANTIES
620              
621             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
622             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
623             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
624