File Coverage

blib/lib/RDF/Lazy.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1 4     4   65675 use strict;
  4         9  
  4         131  
2 4     4   18 use warnings;
  4         7  
  4         195  
3             package RDF::Lazy;
4             {
5             $RDF::Lazy::VERSION = '0.081';
6             }
7             #ABSTRACT: Lazy typing access to RDF data
8              
9 4     4   56 use v5.10.1;
  4         25  
  4         160  
10 4     4   6295 use RDF::Trine::Model;
  0            
  0            
11             use RDF::NS qw(20120827);
12             use CGI qw(escapeHTML);
13              
14             use RDF::Trine::Serializer::RDFXML;
15             use RDF::Trine::Serializer::Turtle;
16             use RDF::Trine::Serializer::RDFJSON;
17             use RDF::Trine::Parser;
18              
19             use RDF::Lazy::Node;
20             use Scalar::Util qw(blessed refaddr);
21             use Carp qw(carp croak);
22              
23             our $AUTOLOAD;
24              
25             sub str {
26             shift->size . " triples";
27             }
28              
29             sub new {
30             my $class = shift;
31             my ($rdf, %args) = (@_ % 2) ? @_ : (undef,@_);
32              
33             if (defined $args{rdf}) {
34             croak 'Either use first argument or ref => $rdf' if $rdf;
35             $rdf = $args{rdf};
36             }
37              
38             my $namespaces = $args{namespaces} || RDF::NS->new('any');
39             if (blessed($namespaces) and $namespaces->isa('RDF::NS')) {
40             # use reference
41             } elsif (ref($namespaces)) {
42             $namespaces = bless { %$namespaces }, 'RDF::NS';
43             } else {
44             $namespaces = RDF::NS->new($namespaces);
45             }
46              
47             my $self = bless {
48             namespaces => $namespaces,
49             }, $class;
50              
51             $self->cache( delete $args{cache} ) if $args{cache};
52              
53             if (blessed $rdf) {
54             # add model by reference
55             if ($rdf->isa('RDF::Trine::Model')) {
56             $self->{model} = $rdf; # model added by reference
57             } elsif ($rdf->isa('RDF::Trine::Store')) {
58             $self->{model} = RDF::Trine::Model->new($rdf);
59             } elsif($rdf->isa('URI') or $rdf->isa('RDF::Trine::Node::Resource')) {
60             $rdf = $rdf->as_string;
61             } else {
62             $rdf = undef;
63             }
64             }
65              
66             if ($rdf and $rdf =~ /^http:\/\//) {
67             $self->{model} = RDF::Trine::Model->new;
68             $self->load( $rdf );
69             }
70              
71             croak 'expected RDF::Trine::Model, RDF::Trine::Store or URI'
72             unless $rdf // 1;
73              
74             if ( not $self->{model} ) {
75             $self->{model} = RDF::Trine::Model->new;
76             $self->add( $rdf, %args );
77             }
78              
79             $self;
80             }
81              
82              
83             sub cache {
84             my $self = shift;
85             if (@_) {
86             my $c = shift;
87             croak "cache must support 'get' and 'set' methods"
88             unless blessed $c and $c->can('get') and $c->can('set');
89             $self->{cache} = $c;
90             }
91             $self->{cache};
92             }
93              
94              
95             sub load {
96             my ($self, $uri) = @_;
97              
98             my $size = $self->{model}->size;
99              
100             if ($self->cache) {
101             my $format = 'Turtle'; # cache must be purged if format changes!
102              
103             my $rdf = $self->cache->get( $uri );
104             if ($rdf) {
105             RDF::Trine::Parser->new($format)
106             ->parse_into_model( $uri, $rdf, $self->{model} );
107             } else {
108             my $model = RDF::Trine::Model->new;
109             RDF::Trine::Parser->parse_url_into_model( $uri, $model );
110             $self->{model}->add_iterator( $model->as_stream );
111              
112             $rdf = RDF::Trine::Serializer->new($format)
113             ->serialize_model_to_string( $model );
114             $self->cache->set( $uri, $rdf );
115             }
116             } else {
117             RDF::Trine::Parser->parse_url_into_model( $uri, $self->{model} );
118             }
119              
120             return ($self->{model}->size - $size);
121             }
122              
123             # method includes parts of RDF::TrineShortcuts::rdf_parse by Toby Inkster
124             sub add { # rdf by value
125             my $self = shift;
126              
127             # TODO: have a look at RDF::TrineShortcuts::rdf_parse
128              
129             if (@_ == 3 and $_[1] !~ /^[a-z]+$/) { # TODO: allow 'a'?
130             my @triple = @_;
131             @triple = map { $self->uri($_) } @triple;
132             if ( grep { not defined $_ } @triple ) {
133             croak 'Failed to add pseudo-triple';
134             }
135             @triple = map { $_->trine } @triple;
136             my $stm = RDF::Trine::Statement->new( @triple );
137             $self->model->add_statement( $stm );
138             return;
139             }
140              
141             my ($rdf, %args) = @_;
142              
143             if (blessed $rdf) {
144             if ($rdf->isa('RDF::Trine::Graph')) {
145             $rdf = $rdf->get_statements;
146             }
147             if ($rdf->isa('RDF::Trine::Iterator::Graph')) {
148             $self->model->begin_bulk_ops;
149             while (my $row = $rdf->next) {
150             $self->model->add_statement( $row );
151             }
152             $self->model->end_bulk_ops;
153             } elsif ($rdf->isa('RDF::Trine::Statement')) {
154             $self->model->add_statement( $rdf );
155             } elsif ($rdf->isa('RDF::Trine::Model')) {
156             $self->add( $rdf->as_stream );
157             } else {
158             croak 'Cannot add RDF object of type ' . ref($rdf);
159             }
160             } elsif ( ref $rdf ) {
161             if ( ref $rdf eq 'HASH' ) {
162             $self->model->add_hashref($rdf);
163             } else {
164             croak 'Cannot add RDF object of type ' . ref($rdf);
165             }
166             } else {
167             # TODO: parse from file, glob, or string in Turtle syntax or other
168             # reuse namespaces if parsing Turtle or SPARQL
169              
170             my $format = $args{format} || 'turtle';
171             my $base = $args{base} || 'http://localhost/';
172             my $parser = RDF::Trine::Parser->new( $format );
173             $parser->parse_into_model( $base, $rdf, $self->model );
174             }
175             }
176              
177             sub query {
178             # TODO: See RDF::TrineShortcuts::rdf_query
179             carp __PACKAGE__ . '::query not implemented yet';
180             }
181              
182             *sparql = *query;
183              
184             sub model { $_[0]->{model} }
185              
186             sub size { $_[0]->{model}->size }
187              
188             sub rels { shift->_relrev( 1, 'rel', @_ ); }
189             sub rel { shift->_relrev( 0, 'rel', @_ ); }
190             sub rev { shift->_relrev( 0, 'rev', @_ ); }
191             sub revs { shift->_relrev( 1, 'rev', @_ ); }
192              
193             sub turtle {
194             my $self = shift;
195             $self->_serialize(
196             RDF::Trine::Serializer::Turtle->new( namespaces => $self->{namespaces} ),
197             @_
198             );
199             }
200              
201             *ttl = *turtle;
202              
203             sub rdfjson {
204             shift->_serialize( RDF::Trine::Serializer::RDFJSON->new, @_ );
205             }
206              
207             sub rdfxml {
208             my $self = shift;
209             $self->_serialize(
210             RDF::Trine::Serializer::RDFXML->new( namespaces => $self->{namespaces} ),
211             @_
212             );
213             }
214              
215             sub ttlpre {
216             return '<pre class="turtle">'
217             . escapeHTML( "# " . ($_[0]->str||'') . "\n" . turtle(@_) )
218             . '</pre>';
219             }
220              
221             sub resource { RDF::Lazy::Resource->new( @_ ) }
222             sub literal { RDF::Lazy::Literal->new( @_ ) }
223             sub blank { RDF::Lazy::Blank->new( @_ ) }
224              
225             sub node {
226             carp __PACKAGE__ . '::node is depreciated - use ::uri instead!';
227             uri(@_);
228             }
229              
230             sub uri {
231             my ($self,$node) = @_;
232             return unless defined $node;
233              
234             if (blessed $node) {
235             if ($node->isa('RDF::Lazy::Node')) {
236             # copy from another or from this graph
237             # return $node if refaddr($node->graph) eq refaddr($self);
238             $node = $self->trine;
239             }
240             if ($node->isa('RDF::Trine::Node::Resource')) {
241             return RDF::Lazy::Resource->new( $self, $node );
242             } elsif ($node->isa('RDF::Trine::Node::Literal')) {
243             return RDF::Lazy::Literal->new( $self, $node );
244             } elsif ($node->isa('RDF::Trine::Node::Blank')) {
245             return RDF::Lazy::Blank->new( $self, $node );
246             } else {
247             carp 'Cannot create RDF::Lazy::Node from ' . ref($node);
248             return;
249             }
250             }
251              
252             my ($prefix,$local,$uri);
253              
254             if ( $node =~ /^<(.*)>$/ ) {
255             return RDF::Lazy::Resource->new( $self, $1 );
256             } elsif ( $node =~ /^_:(.*)$/ ) {
257             return RDF::Lazy::Blank->new( $self, $1 );
258             } elsif ( $node =~ /^\[\s*\]$/ ) {
259             return RDF::Lazy::Blank->new( $self );
260             } elsif ( $node =~ /^["'+-0-9]|^(true|false)$/ ) {
261             return $self->_literal( $node );
262             } elsif ( $node =~ /^([^:]*):([^:]*)$/ ) {
263             ($prefix,$local) = ($1,$2);
264             } elsif ( $node =~ /^(([^_:]*)_)?([^_:]+.*)$/ ) {
265             ($prefix,$local) = ($2,$3);
266             } else {
267             return;
268             }
269              
270             $prefix = "" unless defined $prefix;
271             # if (defined $prefix) {
272             $uri = $self->{namespaces}->URI("$prefix:$local");
273             # } else {
274             # # Bug in RDF::Trine::NamespaceMap, line 133 - wait until fixed
275             # # $predicate = $self->{namespaces}->uri(":$local");
276             # my $ns = $self->{namespaces}->namesespace_uri("");
277             # $uri = $ns->uri($local) if defined $ns;
278             #}
279              
280             return unless defined $uri;
281             return RDF::Lazy::Resource->new( $self, $uri );
282             }
283              
284             sub namespaces {
285             return shift->{namespaces};
286             }
287              
288             sub ns {
289             my $self = shift;
290             return unless @_;
291              
292             if (@_ == 2) { # set
293             $self->{namespaces}->{$_[0]} = $_[1];
294             $self->{nsprefix}->{$_[1]} = $_[0] if $self->{nsprefix};
295             return;
296             }
297             return $self->{namespaces}->{$_[0]}
298             if $_[0] !~ ':'; # get namespace
299             $self->{nsprefix} = $self->{namespaces}->REVERSE
300             unless $self->{nsprefix};
301             return $self->{nsprefix}->{$_[0]};
302             }
303              
304             sub subjects {
305             my $self = shift;
306             my ($predicate, $object) = map { my $self->uri($_)->trine } @_;
307             return map { $self->uri($_) } $self->model->subjects( $predicate, $object );
308             }
309              
310             sub predicates {
311             my $self = shift;
312             my ($subject, $object) = map { $self->uri($_)->trine } @_;
313             return map { $self->uri($_) } $self->model->predicates( $subject, $object );
314             }
315              
316             sub objects {
317             my ($self, $subject, $predicate, %options) = @_;
318             ($subject, $predicate) = map { $self->uri($_)->trine } ($subject, $predicate);
319             return map { $self->uri($_) } $self->model->objects( $subject, $predicate, %options );
320             }
321              
322             sub AUTOLOAD {
323             my $self = shift;
324             return if !ref($self) or $AUTOLOAD =~ /^(.+::)?DESTROY$/;
325              
326             my $name = $AUTOLOAD;
327             $name =~ s/.*:://;
328              
329             return $self->uri($name);
330             }
331              
332             ### internal methods
333              
334             # parts from RDF/Trine/Parser/Turtle.pm
335             my $xsd = RDF::Trine::Namespace->new('http://www.w3.org/2001/XMLSchema#');
336             #my $r_language = qr'[a-z]+(-[a-z0-9]+)*'i;
337             my $r_double = qr'^[+-]?([0-9]+\.[0-9]*[eE][+-]?[0-9]+|\.[0-9]+[eE][+-]?[0-9]+|[0-9]+[eE][+-]?[0-9]+)$';
338             my $r_decimal = qr'^[+-]?([0-9]+\.[0-9]*|\.([0-9])+)$';
339             my $r_integer = qr'^[+-]?[0-9]+';
340             my $r_boolean = qr'^(true|false)$';
341             my $r_string1 = qr'^"(.*)"(\@([a-z]+(-[a-z0-9]+)*))?$'i;
342             my $r_string2 = qr'^"(.*)"(\@([a-z]+(-[a-z0-9]+)*))?$'i;
343              
344             sub _literal {
345             my ($self, $s) = @_;
346              
347             my ($literal, $language, $datatype);
348              
349             if ( $s =~ $r_string1 or $s =~ $r_string2 ) {
350             ($literal, $language) = ($1,$3);
351             } elsif( $s =~ $r_double ) {
352             $literal = $s;
353             $datatype = $xsd->double;
354             } elsif( $s =~ $r_decimal ) {
355             $literal = $s;
356             $datatype = $xsd->decimal;
357             } elsif( $s =~ $r_integer ) {
358             $literal = $s;
359             $datatype = $xsd->integer;
360             } elsif( $s =~ $r_boolean ) {
361             $literal = $s;
362             $datatype = $xsd->boolean;
363             }
364              
365             return $self->literal( $literal, $language, $datatype );
366             }
367              
368             sub _query {
369             my ($self,$all,$dir,$subject,$property,@filter) = @_;
370              
371             $subject = $self->uri($subject)
372             unless blessed($subject) and $subject->isa('RDF::Lazy::Node');
373              
374             $property = $self->uri($property) if defined $property;
375             $property = $property->trine if defined $property;
376              
377             my @res;
378              
379             if ($dir eq 'rel') {
380             @res = $self->{model}->objects( $subject->trine, $property );
381             } elsif ($dir eq 'rev') {
382             @res = $self->{model}->subjects( $property, $subject->trine );
383             }
384              
385             @res = map { $self->uri( $_ ) } @res;
386              
387             # TODO apply filters one by one and return in order of filters
388             @res = grep { $_->is(@filter) } @res if @filter;
389              
390             return $all ? \@res : $res[0];
391             }
392              
393             sub _relrev {
394             my $self = shift;
395             my $all = shift;
396             my $type = shift;
397             my $subject = shift;
398              
399             if (@_) {
400             # get objects / subjects
401             my ($property,@filter) = @_;
402             $all = 1 if ($property and not ref $property and $property =~ s/^(.+[^_])_$/$1/);
403             return $self->_query( $all, $type, $subject, $property, @filter );
404             } else {
405             # get all predicates
406             $subject = $self->uri($subject)
407             unless blessed($subject) and $subject->isa('RDF::Lazy::Node');
408              
409             my @res;
410              
411             if ($type eq 'rel') {
412             @res = $self->{model}->predicates( $subject->trine, undef );
413             } elsif ($type eq 'rev') {
414             @res = $self->{model}->predicates( undef, $subject->trine );
415             }
416              
417             return $all ? [ map { $self->uri( $_ ) } @res ] : $self->uri( $res[0] );
418             }
419             }
420              
421             sub _serialize {
422             my ($self, $serializer, $subject) = @_;
423             my $iterator;
424              
425             if ($subject) {
426             $subject = $self->uri($subject)
427             unless blessed($subject) and $subject->isa('RDF::Lazy::Node');
428             $iterator = $self->{model}->bounded_description( $subject->trine );
429             } else {
430             $iterator = $self->model->as_stream;
431             }
432              
433             return $serializer->serialize_iterator_to_string( $iterator );
434             }
435              
436             1;
437              
438              
439              
440             =pod
441              
442             =head1 NAME
443              
444             RDF::Lazy - Lazy typing access to RDF data
445              
446             =head1 VERSION
447              
448             version 0.081
449              
450             =head1 SYNOPSIS
451              
452             ### How to create a graph
453              
454             $g = RDF::Lazy->new(
455             rdf => $data, # RDF::Trine::Model or ::Store (by reference)
456             namespaces => { # namespace prefix, RDF::NS or RDF::Trine::NamespaceMap
457             foaf => 'http://xmlns.com/foaf/0.1/',
458             rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
459             xsd => "http://www.w3.org/2001/XMLSchema#",
460             }
461             );
462              
463             $g = RDF::Lazy->new( $data, format => 'turtle' ); # parse RDF/Turtle
464             $g = RDF::Lazy->new( $data, format => 'rdfxml' ); # parse RDF/XML
465             $g = RDF::Lazy->new( "http://example.org/" ); # retrieve LOD
466              
467             ### How to get nodes
468              
469             $p = $g->resource('http://xmlns.com/foaf/0.1/Person'); # get node
470             $p = $g->uri('<http://xmlns.com/foaf/0.1/Person>'); # alternatively
471             $p = $g->uri('foaf:Person); # same but lazier
472             $p = $g->foaf_Person; # same but laziest
473              
474             $l = $g->literal('Alice'); # get literal node
475             $l = $g->literal('Alice','en'); # get literal node with language
476             $l = $g->literal('123','xsd:integer'); # get literal node with datatype
477              
478             $b = $g->blank('x123'); # get blank node
479             $b = $g->blank; # get blank node with random id
480              
481             ### How to retrieve RDF
482              
483             $x->rel('foaf:knows'); # retrieve a person that $x knows
484             $x->rev('foaf:knows'); # retrieve a person known by $x
485              
486             $x->rels('foaf:knows'); # retrieve all people that $x knows
487             $x->revs('foaf:knows'); # retrieve all people known by $x
488              
489             $x->foaf_knows; # short form of $x->rel('foaf:knows')
490             $x->foaf_knows_; # short form of $x->rels('foaf:knows')
491              
492             $x->rels; # array reference with a list of properties
493             $x->revs; # same as rels, but other direction
494              
495             $x->type; # same as $x->rel('rdf:type')
496             $x->types; # same as $x->rels('rdf:type')
497              
498             $g->subjects( 'rdf:type', 'foaf:Person' ); # retrieve subjects
499             $g->predicates( $subject, $object ); # list predicates
500             $g->objects( $subject, 'foaf:knows' ); # list objects
501              
502             ### How to add RDF
503              
504             $g->add( $rdfdata, format => 'rdfxml' ); # parse and add
505             $g->add( $subject, $predicate, $object ); # add single triple
506              
507             ### How to show RDF
508              
509             $g->turtle; # dump in RDF/Turtle syntax
510             $g->ttlpre; # dump in RDF/Turtle, wrapped in a HTML <pre> tag
511             $g->rdfxml; # dump in RDF/XML
512             $g->rdfjson; # dump in RDF/JSON
513              
514             =head1 DESCRIPTION
515              
516             This module wraps L<RDF::Trine::Node> to provide simple node-centric access to
517             RDF data. It was designed to access RDF within L<Template> Toolkit but the
518             module does not depend on or and can be used independently. Basically, an
519             instance of RDF::Lazy contains an unlabeled RDF graph and a set of namespace
520             prefixes. For lazy access and graph traversal, each RDF node
521             (L<RDF::Lazy::Node>) is tied to the graph.
522              
523             =head1 METHODS
524              
525             =head2 cache( [ $cache ] )
526              
527             Get and/or set a cache for loading RDF from URIs or URLs. A C<$cache> can be
528             any blessed object that supports method C<get($uri)> and C<set($uri,$value)>.
529             For instance one can enable a simple file cache with L<CHI> like this:
530              
531             my $rdf = RDF::Lazy->new(
532             cache => CHI->new(
533             driver => 'File', root_dir => '/tmp/cache',
534             expires_in => '1 day'
535             )
536             );
537              
538             By default, RDF is stored in Turtle syntax for easy inspection.
539              
540             =head2 load( $uri )
541              
542             Load RDF from an URI or URL. RDF data is optionally retrieved from a cache.
543             Returns the number of triples that have been added (which could be zero if
544             all loaded triples are duplicates).
545              
546             =head2 new ( [ [ rdf => ] $rdf ] [, namespaces => $namespaces ] [ %options ])
547              
548             Return new RDF graph. Namespaces can be provided as hash reference or as
549             L<RDF::Trine::NamespaceMap> or L<RDF::NS>. By default, the current local
550             version of RDF::NS is used. RDF data can be L<RDF:Trine::Model> or
551             L<RDF::Trine::Store>, which are used by reference, or many other forms, as
552             supported by L<add|/add>.
553              
554             =head2 resource ( $uri )
555              
556             Return L<RDF::Lazy::Resource> node. The following statements are equivalent:
557              
558             $graph->resource('http://example.org');
559             $graph->uri('<http://example.org>');
560              
561             =head2 literal ( $string , $language_or_datatype, $datatype )
562              
563             Return L<RDF::Lazy::Literal> node.
564              
565             =head2 blank ( [ $identifier ] )
566              
567             Return L<RDF::Lazy::Blank> node. A random identifier is generated unless you
568             provide an identifier as parameter.
569              
570             =head2 uri ( $name | $node )
571              
572             Returns a node that is connected to the graph. Note that every valid RDF node
573             is part of any RDF graph: this method does not check whether the graph actually
574             contains a triple with the given node. You can either pass a name or an
575             instance of L<RDF::Trine::Node>. This method is also called for any undefined
576             method, so the following statements are equivalent:
577              
578             $graph->true;
579             $graph->uri('true');
580              
581             =head2 rel / rels / rev / revs
582              
583             Can be used to traverse the graph. See L<RDF::Lazy::Node>:
584              
585             $node->rel( ... ) # where $node is located in $graph
586             $graph->rel( $node, ... ) # equivalent
587              
588             =head2 add
589              
590             Add RDF data. I<Sorry, not documented yet!>
591              
592             =head2 ttl ( [ $node ] )
593              
594             Returns a RDF/Turtle representation of a node's bounded description.
595              
596             =head2 ttlpre ( [ $node ] )
597              
598             Returns an HTML escaped RDF/Turtle representation of a node's bounded
599             description, wrapped in a HTML C<< <pre class="turtle"> >> element.
600              
601             =head2 ns ( $prefix | $namespace | $prefix => $namespace )
602              
603             Gets or sets a namespace mapping for the entire graph. By default, RDF::Lazy
604             makes use of popular namespaces defined in L<RDF::NS>.
605              
606             $g->ns('dc'); # returns 'http://purl.org/dc/elements/1.1/'
607             $g->ns('http://purl.org/dc/elements/1.1/'); # returns 'dc'
608             $g->ns( dc => 'http://example.org/' ); # modify mapping
609              
610             =head1 SEE ALSO
611              
612             L<RDF::Helper> and L<RDF::TrineShortcuts> provide similar APIs. Another similar framework
613             for PHP and Python is Graphite: http://graphite.ecs.soton.ac.uk/,
614             http://code.google.com/p/python-graphite/.
615              
616             =head1 AUTHOR
617              
618             Jakob Voß <voss@gbv.de>
619              
620             =head1 COPYRIGHT AND LICENSE
621              
622             This software is copyright (c) 2013 by Jakob Voß.
623              
624             This is free software; you can redistribute it and/or modify it under
625             the same terms as the Perl 5 programming language system itself.
626              
627             =cut
628              
629              
630             __END__
631