File Coverage

blib/lib/Class/RDF.pm
Criterion Covered Total %
statement 76 330 23.0
branch 0 130 0.0
condition 0 39 0.0
subroutine 26 66 39.3
pod n/a
total 102 565 18.0


line stmt bran cond sub pod time code
1             package Class::RDF::Store;
2              
3 1     1   1128 use base "Class::DBI";
  1         3  
  1         1585  
4 1     1   134806 use File::Temp;
  1         45406  
  1         259  
5              
6             our @Create_SQL = (<<'', <<'', <<'', <<'');
7             create table ns (
8             prefix char(16),
9             uri char(255)
10             );
11              
12             create table node (
13             id integer primary key,
14             created timestamp,
15             value text,
16             is_resource integer(1)
17             );
18              
19             create table statement (
20             id integer primary key,
21             created timestamp,
22             subject integer,
23             predicate integer,
24             object integer,
25             context integer
26             );
27              
28             create table metastatement (
29             id integer primary key,
30             created timestamp,
31             subject integer,
32             predicate integer,
33             object integer
34             );
35              
36              
37             sub is_transient {
38 0     0     my $class = shift;
39 0           my %args = ( TEMPLATE => "crdfXXXX", SUFFIX => ".db", UNLINK => 1, @_ );
40 0           my $tmp = File::Temp->new( %args );
41              
42 0           $class->set_db( Main => "dbi:SQLite:".$tmp->filename, "", "" );
43              
44 0           for my $st (@Create_SQL) {
45 0           $class->db_Main->do($st);
46             }
47             }
48              
49             package Class::RDF::NS;
50              
51 1     1   23 use Carp;
  1         3  
  1         71  
52 1     1   6 use base 'Class::RDF::Store';
  1         2  
  1         636  
53 1     1   6 use vars '$AUTOLOAD';
  1         2  
  1         49  
54 1     1   5 use strict;
  1         2  
  1         44  
55 1     1   5 use warnings;
  1         2  
  1         77  
56 1     1   5 no warnings 'redefine';
  1         2  
  1         274  
57              
58             __PACKAGE__->table( "ns" );
59             __PACKAGE__->columns( All => qw( prefix uri ) );
60              
61             our (%Cache, $Prefix_RE);
62              
63             sub define {
64 0     0     my ($class, %uri) = @_;
65 0           while (my ($prefix, $uri) = each %uri) {
66 0           my $ns = $class->find_or_create({ prefix => $prefix });
67 0           $Cache{$prefix} = $ns;
68 0           $ns->uri( $uri );
69 0           $ns->update;
70             }
71 0           $class->_build_prefix_re;
72             }
73              
74             sub export {
75 0     0     my ($class, @prefixes) = @_;
76 0           for my $prefix (@prefixes) {
77 0           my $ns = $class->retrieve($prefix);
78 0 0         croak "Can't find prefix $prefix" unless $ns;
79 0           my $uri = $ns->uri;
80              
81 1     1   5 no strict;
  1         1  
  1         625  
82 0           *{"$prefix\::AUTOLOAD"} = sub {
83 0     0     my $object = shift;
84 0           (my $prop = $AUTOLOAD) =~ s/^.*:://o;
85 0 0         if (ref($object)) {
86 0           $object->get_or_set( "$uri$prop", @_ );
87             } else {
88 0           return "$uri$prop";
89             }
90 0           };
91             }
92             }
93              
94             sub retrieve {
95 0     0     my ($class, $prefix) = @_;
96 0 0         return $Cache{$prefix} if $Cache{$prefix};
97 0           my $ns = $class->SUPER::retrieve($prefix);
98 0           $Cache{$prefix} = $ns;
99 0           $class->_build_prefix_re;
100             }
101              
102             sub load {
103 0     0     my $class = shift;
104 0           my $iter = $class->retrieve_all;
105 0           while (my $ns = $iter->next) {
106 0           $Cache{$ns->prefix} = $ns
107             }
108 0           $class->_build_prefix_re;
109             }
110              
111             sub expand {
112 0     0     my ($class, $uri) = @_;
113 0           $uri =~ s/$Prefix_RE/$Cache{$1}->uri/es;
  0            
114 0           return $uri;
115             }
116              
117             sub _build_prefix_re {
118 0     0     my $class = shift;
119 0           my $list = join("|", keys %Cache);
120 0           $Prefix_RE = qr/^($list):/;
121             }
122              
123             package Class::RDF::Node;
124              
125 1     1   6 use base "Class::RDF::Store";
  1         2  
  1         712  
126 1         13 use overload '""' => \&as_string,
127 1     1   7 eq => \&_string_eq;
  1         1  
128            
129 1     1   103 use warnings;
  1         1  
  1         24  
130 1     1   4 use strict;
  1         2  
  1         542  
131              
132             __PACKAGE__->table( "node" );
133             __PACKAGE__->columns( All => qw( id created value is_resource ) );
134             __PACKAGE__->autoupdate(1);
135             our %Cache;
136              
137             sub new {
138 0     0     my $class = shift;
139 0   0       my $value = shift || "";
140 0 0 0       return $class if ref $class and $class->isa(__PACKAGE__);
141              
142 0           my $cached = $class->cache($value);
143 0 0         return $cached if $cached;
144              
145 0 0         my $is_resource = ($value =~ /^\w+:\S+$/o ? 1 : 0);
146 0           my $obj =$class->find_or_create({ value => $value, is_resource => $is_resource });
147 0           return $class->cache($value, $obj);
148             }
149              
150             sub find {
151 0     0     my ($class,$value) = @_;
152 0 0         return unless defined $value;
153            
154 0           my $cached = $class->cache($value);
155 0 0         return $cached if $cached;
156              
157 0           my ($found) = $class->search({ value => $value });
158 0 0         $class->cache($value, $found) if $found;
159 0           return $found;
160             }
161              
162             sub cache {
163 0     0     my ($class, $value, $node) = @_;
164 0 0         if ($node) {
    0          
165 0           return ($Cache{$value} = $node);
166             } elsif (exists $Cache{$value}) {
167 0           return $Cache{$value}
168             } else {
169 0           return undef;
170             }
171             }
172              
173             sub as_string {
174 0     0     my $self = shift;
175 0           return $self->value;
176             }
177             sub _string_eq {
178 0     0     my ($self,$other) = @_;
179 0           $self->as_string eq $other;
180             }
181              
182             package Class::RDF::Statement;
183              
184 1     1   6 use base "Class::RDF::Store";
  1         2  
  1         538  
185 1     1   6 use warnings;
  1         2  
  1         28  
186 1     1   5 use strict;
  1         2  
  1         40  
187              
188 1     1   5 use constant Node => "Class::RDF::Node";
  1         2  
  1         76  
189 1     1   4 use constant Object => "Class::RDF::Object";
  1         2  
  1         1294  
190              
191             our @Quad = qw( subject predicate object context );
192              
193             __PACKAGE__->table( "statement" );
194             __PACKAGE__->columns( All => "id", "created", @Quad );
195             __PACKAGE__->has_a( $_ => Node ) for @Quad;
196             __PACKAGE__->autoupdate(1);
197              
198             __PACKAGE__->set_sql( RetrieveFull => <<"" );
199             SELECT st.*, n.*
200             FROM statement st, node n
201             WHERE %s
202             AND ( st.subject = n.id
203             OR st.predicate = n.id
204             OR st.object = n.id
205             OR st.context = n.id )
206              
207              
208             __PACKAGE__->set_sql( RetrieveOrdered => <<"" );
209             SELECT st.*, n.*
210             FROM statement st, node n, node m
211             WHERE %s
212             AND ( st.subject = n.id
213             OR st.predicate = n.id
214             OR st.object = n.id
215             OR st.context = n.id )
216             AND m.id = object
217             ORDER BY m.value %s
218              
219             __PACKAGE__->set_sql( RetrieveObjects => <<"" );
220             SELECT obj.*, n.*
221             FROM statement st, statement obj, node n
222             WHERE %s
223             AND obj.subject = st.subject
224             AND ( obj.subject = n.id
225             OR obj.predicate = n.id
226             OR obj.object = n.id
227             OR obj.context = n.id )
228              
229             sub new {
230 0     0     my ($class, @nodes) = @_;
231              
232 0           my @triple;
233 0           for my $node (@nodes) {
234 0 0         $node = $class->Node->new($node) unless ref $node;
235 0           push @triple, $node;
236             }
237 0           $class->find_or_create({ subject => $triple[0],
238             predicate => $triple[1],
239             object => $triple[2],
240             context => $triple[3] });
241              
242             }
243              
244             sub value {
245 0     0     my $self = shift;
246 0           my $obj = $self->object;
247 0 0         return $obj->is_resource ?
248             $self->Object->new($obj->value) : $obj->value;
249             }
250              
251             sub triples {
252 0     0     my $self = shift;
253 0           my @t;
254 0           foreach (qw(subject predicate object)) {
255 0           my $node = $self->$_;
256 0 0 0       if ($node and $node->can('value')) {
257 0           push @t, $node->value;
258             }
259             else {
260             # XXX: why are we returning undef here?
261 0           return undef;
262             }
263             }
264 0           return @t;
265             }
266              
267             sub search {
268 0     0     my $class = shift;
269 0 0         my %args = ref($_[0]) ? %{$_[0]} : @_;
  0            
270              
271 0 0         $args{like} = delete $args{object} if $args{like};
272              
273 0           my ($where, $vals) = $class->compose_query(%args);
274 0 0         return $class->no_match unless ref $where;
275              
276 0 0         if ( $args{like} ) {
277 0           my @nodes = $class->Node->search_like( value => '%'.$args{like}.'%' );
278 0 0         return $class->no_match unless @nodes;
279 0           push @$where, "st.object IN (" . join(",", map($_->id, @nodes)) . ")";
280             }
281              
282 0           my $sth;
283 0 0         if ( $args{objects} ) {
    0          
284 0           $sth = $class->sql_RetrieveObjects( join(" AND ", @$where) );
285             } elsif ( $args{order} ) {
286 0           $sth = $class->sql_RetrieveOrdered( join(" AND ", @$where), $args{order} );
287             } else {
288 0           $sth = $class->sql_RetrieveFull( join(" AND ", @$where) );
289             }
290              
291 0           return $class->retrieve_from_sth($sth, @$vals);
292             }
293              
294             sub no_match {
295 0     0     my $class = shift;
296 0           $class->_ids_to_objects([]);
297             }
298              
299             sub compose_query {
300 0     0     my ($class, %args) = @_;
301 0           my (@where, @vals);
302              
303 0 0 0       $args{predicate} = Class::RDF::NS->expand( $args{predicate} )
304             if exists $args{predicate} and not ref $args{predicate};
305              
306 0           for my $position (grep exists $args{$_}, @Quad) {
307 0           push @where, "st.$position = ?";
308 0 0         if ( ref $args{$position} ) {
309 0           push @vals, $args{$position};
310             } else {
311 0 0         my $node = $class->Node->find( $args{$position} ) or return;
312 0           push @vals, $node;
313             }
314             }
315              
316 0           return (\@where, \@vals);
317             }
318              
319             sub retrieve_from_sth {
320 0     0     my ($class, $sth, @bind) = @_;
321              
322 0           my (@results, %nodes, %triples, %t, %n);
323 0           eval {
324 0           $sth->execute( map($_->id, @bind) );
325 0           $sth->bind_columns(\(
326             @t{qw{ id created subject predicate object context }},
327             @n{qw{ id created value is_resource }} ));
328              
329 0           while ($sth->fetch) {
330 0 0         unless ( exists $nodes{$n{id}} ) {
331 0           $nodes{$n{id}} = $class->Node->construct(\%n);
332             }
333 0 0         unless ( exists $triples{$t{id}} ) {
334 0           push @results, ( $triples{$t{id}} = {%t} );
335             }
336             }
337             };
338              
339 0 0         return $class->_croak("$class can't $sth->{Statement}: $@", err => $@)
340             if $@;
341              
342 0           for my $st (values %triples) {
343 0           for my $which (@Quad) {
344 0 0         $st->{$which} = $nodes{$st->{$which}} if $st->{$which};
345             }
346             }
347              
348 0           return $class->_ids_to_objects(\@results);
349             }
350              
351             # ... we need to figure out where this belongs ...
352             #
353             # use Time::Piece;
354             #
355             # sub ical_to_sql {
356             # my ($class,$ical) = @_;
357             # warn($ical);
358             # my $t = Time::Piece->strptime($ical,"%Y%m%dT%H%M%SZ");
359             # $t->strftime("%Y%m%d%H%M%S");
360             # }
361             #
362             # sub timeslice {
363             # my ($self,%p) = @_;
364             # my $start = $p{start};
365             # my $end = $p{end};
366             # my @where;
367             # # SQL for timestamp
368             # warn("time");
369             # push @where, "created > " . $self->ical_to_sql($start) if $start;
370             # push @where, "created < " . $self->ical_to_sql($end) if $end;
371             # my $sql = join(" and ", @where); warn($sql);
372             # my @o = $self->retrieve_from_sql($sql);
373             # }
374              
375             package Class::RDF::Object;
376              
377 1     1   8 use Carp;
  1         2  
  1         109  
378             use overload '""' => \&as_string,
379 1     1   5 eq => sub { $_[0]->as_string eq $_[1] };
  1     0   2  
  1         11  
  0         0  
380 1     1   68 use vars '$AUTOLOAD';
  1         7  
  1         50  
381 1     1   5 use strict;
  1         2  
  1         26  
382 1     1   4 use warnings;
  1         2  
  1         39  
383              
384 1     1   5 use constant Node => "Class::RDF::Node";
  1         23  
  1         51  
385 1     1   5 use constant Statement => "Class::RDF::Statement";
  1         2  
  1         2711  
386              
387             sub new {
388 0     0     my $class = shift;
389 0           my ($uri, $context, $data, $base);
390 0 0         $uri = shift unless ref $_[0] eq "HASH";
391 0 0         $context = shift unless ref $_[0] eq "HASH";
392 0 0         $data = shift if ref $_[0] eq "HASH";
393 0 0         $base = shift if $_[0];
394 0   0       $base ||= '_id:';
395 0   0       $uri ||= $base.sprintf("%08x%04x", time, int rand(0xFFFF));
396 0 0         unless (ref $uri) {
397 0           $uri = $class->Node->new($uri);
398             }
399            
400 0 0 0       $context = $class->Node->find($context)
401             if $context and not ref $context;
402              
403 0   0       my $self = bless {
404             context => $context,
405             uri => $uri,
406             triples => {},
407             stub => 1
408             }, ref($class) || $class;
409            
410 0           while (my ($key, $vals) = each %$data) {
411 0 0         for my $val (ref $vals eq 'ARRAY' ? @$vals : $vals) {
412 0 0 0       $val = $val->{uri}->value if ref($val) and $val->{'uri'};
413 0           my $st = $self->Statement->new( $uri, $key, $val );
414 0           $self->_add_statement($st);
415             }
416             }
417 0           return $self;
418             }
419              
420             sub _fetch_statements {
421 0     0     my $self = shift;
422             # warn "fetch_statements ", $self->uri->value, "\n";
423 0           my $iter = $self->Statement->search( subject => $self->uri );
424 0           while (my $st = $iter->next) {
425 0           $self->_add_statement($st);
426             }
427 0           delete $self->{stub};
428             }
429              
430             sub _add_statement {
431 0     0     my ($self, $statement) = @_;
432 0   0       push @{$self->{triples}{$statement->predicate->value} ||= []}, $statement;
  0            
433             }
434              
435             sub statements {
436 0     0     my $self = shift;
437 0 0         $self->_fetch_statements if $self->{stub};
438 0           return map( @$_, values %{$self->{triples}} );
  0            
439             }
440              
441             sub triples {
442 0     0     my $self = shift;
443 0 0         $self->_fetch_statements if $self->{stub};
444 0           return map( [$_->triples], $self->statements );
445             }
446              
447             sub uri {
448 0     0     my $self = shift;
449             # read only because Goddess help us if an object's URI
450             # changes in mid-flight
451 0           return $self->{uri};
452             }
453              
454             sub as_string {
455 0     0     my $self = shift;
456 0           return $self->uri->as_string;
457             }
458             sub _string_eq {
459 0     0     my ($self,$other) = @_;
460 0           $self->as_string eq $other;
461             }
462              
463             sub context {
464 0     0     my $self = shift;
465 0 0         $self->{context} = shift if @_;
466 0 0         return $self->{context} if $self->{context};
467             }
468              
469             sub get {
470 0     0     my ($self, $prop) = @_;
471 0 0         $self->_fetch_statements if $self->{stub};
472 0 0         my $statements = $self->{triples}{$prop} or return;
473 0           my @vals = map( $_->value, @$statements );
474 0 0         return wantarray ? @vals : $vals[0];
475             }
476              
477             sub set {
478 0     0     my ($self, %args) = @_;
479 0 0         $self->_fetch_statements if $self->{stub};
480 0           while (my ($key, $val) = each %args) {
481 0 0         if (exists $self->{triples}{$key}) {
482 0           $_->delete for @{$self->{triples}{$key}};
  0            
483 0           delete $self->{triples}{$key};
484             }
485              
486 0 0         for my $value (ref($val) eq "ARRAY" ? @$val : $val) {
487 0 0 0       $value = $value->uri if ref($value) and $value->can('uri');
488 0           my $triple = $self->Statement->new(
489             $self->uri->value, $key, $value, $self->context );
490 0           $self->_add_statement( $triple );
491             }
492             }
493             }
494              
495             # delete forward and backward references to me
496             #
497             sub delete {
498 0     0     my $self = shift;
499 0           my $uri = $self->uri->value;
500             # triples that have my subject, and other predicate and object.
501             #
502 0           my @triples = $self->statements;
503 0           foreach (@triples) {
504 0           $_->delete;
505             }
506             # triples that have me as their object
507 0           my @pointers = Class::RDF::Statement->search(object => $uri);
508 0           foreach (@pointers) {
509 0           $_->delete;
510             }
511             # is that really it?
512             }
513              
514             sub get_or_set {
515 0     0     my ($self, $prop, @vals) = @_;
516 0 0         if (@vals) {
517 0           $self->set($prop => shift @vals);
518             } else {
519 0           return $self->get($prop);
520             }
521             }
522              
523             sub add {
524 0     0     my ($self, %args) = @_;
525 0 0         $self->_fetch_statements if $self->{stub};
526 0           while (my ($key, $val) = each %args) {
527 0 0         for my $value (ref($val) eq "ARRAY" ? @$val : $val) {
528 0 0 0       $value = $value->{uri} if ref($value) and $value->{uri};
529            
530 0           my $triple = $self->Statement->new(
531             $self->uri, $key, $value, $self->context );
532 0           $self->_add_statement( $triple );
533             }
534             }
535             }
536              
537             sub remove {
538 0     0     my ($self, %args) = @_;
539 0 0         $self->_fetch_statements if $self->{stub};
540 0           while (my ($key, $vals) = each %args) {
541 0           my %remove;
542 0 0         my @v = ref($vals) eq 'ARRAY' ? @$vals : ($vals);
543 0           foreach my $o (@v) {
544 0 0 0       $o = $o->{uri}->value if ref($o) and $o->{uri};
545 0           $remove{$o} = 1;
546             }
547              
548 0           my $triples = $self->{triples}{$key};
549 0           for (my $st = 0; $st < scalar(@$triples); $st++) {
550 0 0         if ($remove{$triples->[$st]->object->value}) {
551 0           $triples->[$st]->delete;
552 0           splice @$triples, $st--, 1;
553             }
554             }
555             }
556             }
557              
558             sub contains {
559 0     0     my ($self, $prop, $val) = @_;
560 0 0         $self->_fetch_statements if $self->{stub};
561 0 0         return scalar grep( $_ eq $val, @{$self->{triples}{$prop}} )
  0            
562             if exists $self->{triples}{$prop};
563 0           return;
564             }
565              
566             sub find {
567 0     0     my ($class, $uri) = @_;
568 0           my $node = $class->Node->find($uri);
569 0 0         return $node ? $class->new($node) : undef;
570             }
571              
572             sub find_or_create {
573 0     0     my $class = shift;
574 0           my ($args) = @_;
575 0           my $obj;
576              
577 0 0         if (ref $args eq "HASH") {
578 0           ($obj) = $class->search( %$args );
579             } else { # $args is really a uri
580 0           $obj = $class->new( $args );
581             }
582              
583 0   0       $obj ||= $class->new( @_ );
584 0           return $obj;
585             }
586              
587             sub search {
588 0     0     my ($class, $predicate, $object, $args) = @_;
589              
590 0 0         my %args = (ref($args) ? %$args : ());
591 0           $args{predicate} = $predicate;
592 0 0         $args{object} = $object if $object;
593 0           my $iter = $class->Statement->search( %args, objects => 1 );
594              
595 0           my (@results, %seen);
596 0           while (my $st = $iter->next) {
597 0           my $id = $st->subject->id;
598 0 0         unless ( $seen{$id} ) {
599 0           $seen{$id} = $class->new( $st->subject, {} );
600 0           delete $seen{$id}{stub};
601 0           push @results, $seen{$id};
602             }
603 0           $seen{$id}->_add_statement($st);
604             }
605              
606 0 0         if (my $order = $args{order}) {
607 0 0         @results = map { $_->[1] }
  0            
608 0           sort { $order eq "asc" ? $a->[0] cmp $b->[0] : $b->[0] cmp $a->[0] }
609 0           map { [($_->get($predicate))[0], $_] } @results;
610             }
611              
612 0 0         return( wantarray ? @results : $results[0] );
613             }
614              
615             package Class::RDF;
616              
617 1     1   710 use RDF::Simple::Parser;
  0            
  0            
618             use RDF::Simple::Serialiser;
619             use LWP::Simple ();
620             use Carp;
621             use strict;
622             use warnings;
623              
624             use constant Node => "Class::RDF::Node";
625             use constant Statement => "Class::RDF::Statement";
626             use constant Object => "Class::RDF::Object";
627              
628             our ($Parser, $Serializer);
629             our $VERSION = '0.20';
630              
631             sub new {
632             my $class = shift;
633             $class->Object->new( @_ );
634             }
635              
636             sub set_db {
637             my $class = shift;
638             Class::RDF::Store->set_db( Main => @_ );
639             Class::RDF::NS->load;
640             if ( $_[0] =~ /^dbi:Pg:/io ) {
641             $class->Node->sequence( "node_id_seq" );
642             $class->Statement->sequence( "statement_id_seq" );
643             }
644             }
645              
646             sub is_transient {
647             my $class = shift;
648             Class::RDF::Store->is_transient;
649             }
650              
651             sub define {
652             my $class = shift;
653             Class::RDF::NS->define(@_);
654             }
655              
656             sub parser {
657             my $class = shift;
658             $Parser ||= RDF::Simple::Parser->new;
659             return $Parser;
660             }
661              
662             sub serializer {
663             my $class = shift;
664             $Serializer ||= RDF::Simple::Serialiser->new;
665             return $Serializer;
666             }
667              
668             sub parse {
669             my ($class, %args) = @_;
670             my @triples = $args{uri} ?
671             $class->parser->parse_uri($args{uri}) :
672             $class->parser->parse_rdf($args{xml});
673             my %output;
674              
675             return unless @triples;
676              
677             # we care about getting the root object back first
678             my $root = $triples[0][0];
679              
680             $args{context} ||= $args{uri};
681             for my $triple (@triples) {
682             $class->Statement->new(@$triple, $args{context});
683             $output{$triple->[0]}++;
684             }
685              
686             $output{$_} = $class->new($_) for keys %output;
687            
688             my $first = delete $output{$root};
689             return ($first, values %output);
690             }
691              
692             sub serialize {
693             my ($class, @objects) = @_;
694             my @triples;
695             for (@objects) {
696             my @t = $_->triples;
697             push @triples, @t;
698             }
699             $class->serializer->addns( $_->prefix, $_->uri )
700             for Class::RDF::NS->retrieve_all;
701             return $class->serializer->serialise(@triples);
702             }
703              
704             *serialise = *serialise = \&serialize; # because I'm in love with her
705              
706             1;
707              
708             __END__