File Coverage

blib/lib/RDF/Trine/Graph.pm
Criterion Covered Total %
statement 163 175 93.1
branch 40 50 80.0
condition 10 15 66.6
subroutine 26 28 92.8
pod 8 8 100.0
total 247 276 89.4


line stmt bran cond sub pod time code
1             # RDF::Trine::Graph
2             # -----------------------------------------------------------------------------
3              
4             =head1 NAME
5              
6             RDF::Trine::Graph - Materialized RDF Graphs for testing isomorphism
7              
8             =head1 VERSION
9              
10             This document describes RDF::Trine::Graph version 1.018
11              
12             =head1 SYNOPSIS
13              
14             use RDF::Trine::Graph;
15             my $a = RDF::Trine::Graph->new( $model_a );
16             my $b = RDF::Trine::Graph->new( $model_b );
17             print "graphs are " . ($a->equals( $b ) ? "the same" : "different");
18              
19             =head1 DESCRIPTION
20              
21             RDF::Trine::Graph provdes a mechanism for testing graph isomorphism based on
22             graph triples from either a RDF::Trine::Model or a RDF::Trine::Iterator.
23             Isomorphism testing requires materializing all of a graph's triples in memory,
24             and so should be used carefully in situations with large graphs.
25              
26             =head1 METHODS
27              
28             =over 4
29              
30             =cut
31              
32             package RDF::Trine::Graph;
33              
34 68     68   415 use strict;
  68         153  
  68         1686  
35 68     68   321 use warnings;
  68         133  
  68         1701  
36 68     68   315 no warnings 'redefine';
  68         153  
  68         1977  
37              
38 68     68   30065 use Algorithm::Combinatorics qw(permutations);
  68         175317  
  68         5602  
39              
40             our ($VERSION, $debug, $AUTOLOAD);
41             BEGIN {
42 68     68   235 $debug = 0;
43 68         3463 $VERSION = '1.018';
44             }
45              
46             use overload
47 68         797 '==' => \&RDF::Trine::Graph::_eq,
48             'eq' => \&RDF::Trine::Graph::_eq,
49             'le' => \&RDF::Trine::Graph::_le,
50             'ge' => \&RDF::Trine::Graph::_ge,
51             'lt' => \&RDF::Trine::Graph::_lt,
52             'gt' => \&RDF::Trine::Graph::_gt,
53 68     68   476 ;
  68         144  
54              
55             sub _eq {
56 2     2   203 my ($x, $y) = @_;
57 2         6 return $x->equals($y);
58             }
59              
60             sub _le {
61 4     4   12 my ($x, $y) = @_;
62 4         11 return $x->is_subgraph_of($y);
63             }
64              
65             sub _ge {
66 2     2   6 return _le(@_[1,0]);
67             }
68              
69             sub _lt {
70 4     4   12 my ($x, $y) = @_;
71             # Test::More::diag(sprintf('%s // %s', ref($x), ref($y)));
72 4   66     25 return ($x->size < $y->size) && ($x->is_subgraph_of($y));
73             }
74              
75             sub _gt {
76 2     2   8 return _lt(@_[1,0]);
77             }
78              
79 68     68   42364 use Data::Dumper;
  68         241501  
  68         3437  
80 68     68   436 use Log::Log4perl;
  68         143  
  68         445  
81 68     68   3537 use Scalar::Util qw(blessed);
  68         143  
  68         2525  
82 68     68   21888 use RDF::Trine::Node;
  68         231  
  68         3280  
83 68     68   24368 use RDF::Trine::Store;
  68         290  
  68         83505  
84              
85             =item C<< new ( $model ) >>
86              
87             =item C<< new ( $iterator ) >>
88              
89             Returns a new graph from the given RDF::Trine::Model or RDF::Trine::Iterator::Graph object.
90              
91             =cut
92              
93             sub new {
94 33     33 1 1557 my $class = shift;
95 33 100       149 unless (blessed($_[0])) {
96 2         20 throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::new must be called with a Model or Iterator argument";
97             }
98            
99 31         57 my %data;
100 31 100       317 if ($_[0]->isa('RDF::Trine::Iterator::Graph')) {
    100          
101 2         6 my $iter = shift;
102 2         14 my $model = RDF::Trine::Model->new( RDF::Trine::Store->temporary_store() );
103 2         11 while (my $st = $iter->next) {
104 4         15 $model->add_statement( $st );
105             }
106 2         8 $data{ model } = $model;
107             } elsif ($_[0]->isa('RDF::Trine::Model')) {
108 28         104 $data{ model } = shift;
109             } else {
110 1         6 throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::new must be called with a Model or Iterator argument";
111             }
112            
113 30         107 my $self = bless(\%data, $class);
114             }
115              
116             =item C<< equals ( $graph ) >>
117              
118             Returns true if the invocant and $graph represent two equal RDF graphs (e.g.
119             there exists a bijection between the RDF statements of the invocant and $graph).
120              
121             =cut
122              
123             sub equals {
124 11     11 1 3073 my $self = shift;
125 11         20 my $graph = shift;
126 11         56 undef($self->{error});
127 11 100       34 return $self->_check_equality($graph) ? 1 : 0;
128             }
129              
130             sub _check_equality {
131 11     11   21 my $self = shift;
132 11         18 my $graph = shift;
133 11 100 100     61 unless (blessed($graph) and $graph->isa('RDF::Trine::Graph')) {
134 2         4 $self->{error} = "RDF::Trine::Graph::equals must be called with a Graph argument";
135 2         14 throw RDF::Trine::Error::MethodInvocationError -text => $self->{error};
136             }
137            
138 9         26 my @graphs = ($self, $graph);
139 9         26 my ($ba, $nba) = $self->split_blank_statements;
140 9         25 my ($bb, $nbb) = $graph->split_blank_statements;
141 9 100       27 if (scalar(@$nba) != scalar(@$nbb)) {
142 1         3 my $nbac = scalar(@$nba);
143 1         3 my $nbbc = scalar(@$nbb);
144 1         5 $self->{error} = "count of non-blank statements didn't match ($nbac != $nbbc)";
145 1         10 return 0;
146             }
147 8         16 my $bac = scalar(@$ba);
148 8         11 my $bbc = scalar(@$bb);
149 8 100       21 if ($bac != $bbc) {
150 1         5 $self->{error} = "count of blank statements didn't match ($bac != $bbc)";
151 1         8 return 0;
152             }
153            
154 7         15 for ($nba, $nbb) {
155 14         29 @$_ = sort map { $_->as_string } @$_;
  12         28  
156             }
157            
158 7         13 foreach my $i (0 .. $#{ $nba }) {
  7         17  
159 6 100       23 unless ($nba->[$i] eq $nbb->[$i]) {
160 1         6 $self->{error} = "non-blank triples don't match: " . Dumper($nba->[$i], $nbb->[$i]);
161 1         85 return 0;
162             }
163             }
164            
165 6         19 return _find_mapping($self, $ba, $bb);
166             }
167              
168             =item C<< is_subgraph_of ( $graph ) >>
169              
170             Returns true if the invocant is a subgraph of $graph. (i.e. there exists an
171             injection of RDF statements from the invocant to $graph.)
172              
173             =cut
174              
175             sub is_subgraph_of {
176 15     15 1 4243 my $self = shift;
177 15         31 my $graph = shift;
178 15         70 undef($self->{error});
179 15 100       51 return $self->_check_subgraph($graph) ? 1 : 0;
180             }
181              
182             =item C<< injection_map ( $graph ) >>
183              
184             If the invocant is a subgraph of $graph, returns a mapping of blank node
185             identifiers from the invocant graph to $graph as a hashref. Otherwise
186             returns false. The solution is not always unique; where there exist multiple
187             solutions, the solution returned is arbitrary.
188              
189             =cut
190              
191             sub injection_map {
192 0     0 1 0 my $self = shift;
193 0         0 my $graph = shift;
194 0         0 undef($self->{error});
195 0         0 my $map = $self->_check_subgraph($graph);
196 0 0       0 return $map if $map;
197 0         0 return;
198             }
199              
200             sub _check_subgraph {
201 15     15   26 my $self = shift;
202 15         38 my $graph = shift;
203 15 100 100     99 unless (blessed($graph) and $graph->isa('RDF::Trine::Graph')) {
204 2         22 throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::equals must be called with a Graph argument";
205             }
206            
207 13         43 my @graphs = ($self, $graph);
208 13         43 my ($ba, $nba) = $self->split_blank_statements;
209 13         48 my ($bb, $nbb) = $graph->split_blank_statements;
210            
211 13 50       62 if (scalar(@$nba) > scalar(@$nbb)) {
    50          
212 0         0 $self->{error} = "invocant had too many blank node statements to be a subgraph of argument";
213 0         0 return 0;
214             } elsif (scalar(@$ba) > scalar(@$bb)) {
215 0         0 $self->{error} = "invocant had too many non-blank node statements to be a subgraph of argument";
216 0         0 return 0;
217             }
218              
219 13         36 my %NBB = map { $_->as_string => 1 } @$nbb;
  33         93  
220            
221 13         44 foreach my $st (@$nba) {
222 11 100       39 unless ($NBB{ $st->as_string }) {
223 1         14 return 0;
224             }
225             }
226            
227 12         39 return _find_mapping($self, $ba, $bb);
228             }
229              
230             sub _find_mapping {
231 18     18   45 my ($self, $ba, $bb) = @_;
232              
233 18 100       48 if (scalar(@$ba) == 0) {
234 2         22 return {};
235             }
236            
237 16         31 my %blank_ids_a;
238 16         37 foreach my $st (@$ba) {
239 26         70 foreach my $n (grep { $_->isa('RDF::Trine::Node::Blank') } $st->nodes) {
  78         262  
240 30         93 $blank_ids_a{ $n->blank_identifier }++;
241             }
242             }
243              
244 16         29 my %blank_ids_b;
245 16         37 foreach my $st (@$bb) {
246 40         98 foreach my $n (grep { $_->isa('RDF::Trine::Node::Blank') } $st->nodes) {
  120         333  
247 44         104 $blank_ids_b{ $n->blank_identifier }++;
248             }
249             }
250            
251 16         35 my %bb_master = map { $_->as_string => 1 } @$bb;
  40         103  
252            
253 16         62 my @ka = keys %blank_ids_a;
254 16         38 my @kb = keys %blank_ids_b;
255 16         75 my $kbp = permutations( \@kb );
256 16         883 my $count = 0;
257 16         57 MAPPING: while (my $mapping = $kbp->next) {
258 28         409 my %mapping;
259 28         76 @mapping{ @ka } = @$mapping;
260 28 50       79 warn "trying mapping: " . Dumper(\%mapping) if ($debug);
261            
262 28         99 my %bb = %bb_master;
263 28         72 foreach my $st (@$ba) {
264 39         64 my @nodes;
265 39         117 foreach my $method ($st->node_names) {
266 117         325 my $n = $st->$method();
267 117 100       386 if ($n->isa('RDF::Trine::Node::Blank')) {
268 49         129 my $id = $mapping{ $n->blank_identifier };
269 49 50       122 warn "mapping " . $n->blank_identifier . " to $id\n" if ($debug);
270 49         141 push(@nodes, RDF::Trine::Node::Blank->new( $id ));
271             } else {
272 68         146 push(@nodes, $n);
273             }
274             }
275 39         83 my $class = ref($st);
276 39         112 my $mapped_st = $class->new( @nodes )->as_string;
277 39 50       133 warn "checking for '$mapped_st' in " . Dumper(\%bb) if ($debug);
278 39 100       102 if ($bb{ $mapped_st }) {
279 23         77 delete $bb{ $mapped_st };
280             } else {
281 16         108 next MAPPING;
282             }
283             }
284 12 50       31 $self->{error} = "found mapping: " . Dumper(\%mapping) if ($debug);
285 12         229 return \%mapping;
286             }
287            
288 4         49 $self->{error} = "didn't find blank node mapping\n";
289 4         54 return 0;
290             }
291              
292             =item C<< split_blank_statements >>
293              
294             Returns two array refs, containing triples with blank nodes and triples without
295             any blank nodes, respectively.
296              
297             =cut
298              
299             sub split_blank_statements {
300 44     44 1 73 my $self = shift;
301 44         251 my $iter = $self->get_statements;
302 44         97 my (@blanks, @nonblanks);
303 44         141 while (my $st = $iter->next) {
304 135 100       327 if ($st->has_blanks) {
305 75         237 push(@blanks, $st);
306             } else {
307 60         182 push(@nonblanks, $st);
308             }
309             }
310 44         420 return (\@blanks, \@nonblanks);
311             }
312              
313             =item C<< get_statements >>
314              
315             Returns a RDF::Trine::Iterator::Graph object for the statements in this graph.
316              
317             =cut
318              
319             # The code below actually goes further now and makes RDF::Trine::Graph
320             # into a subclass of RDF::Trine::Model via object delegation. This feature
321             # is undocumented as it's not clear whether this is desirable or not.
322              
323             =begin private
324              
325             =item C<< isa >>
326              
327             =cut
328              
329             sub isa {
330 46     46 1 9262 my ($proto, $queried) = @_;
331 46 50       162 $proto = ref($proto) if ref($proto);
332 46   33     286 return UNIVERSAL::isa($proto, $queried) || RDF::Trine::Model->isa($queried);
333             }
334              
335             =item C<< can >>
336              
337             =cut
338              
339             sub can {
340 1     1 1 13 my ($proto, $queried) = @_;
341 1 50       4 $proto = ref($proto) if ref($proto);
342 1   33     11 return UNIVERSAL::can($proto, $queried) || RDF::Trine::Model->can($queried);
343             }
344              
345             sub AUTOLOAD {
346 82     82   9722 my $self = shift;
347 82 100       446 return if $AUTOLOAD =~ /::DESTROY$/;
348 52         369 $AUTOLOAD =~ s/^(.+)::([^:]+)$/$2/;
349 52         240 return $self->{model}->$AUTOLOAD(@_);
350             }
351              
352             =end private
353              
354             =item C<< error >>
355              
356             Returns an error string explaining the last failed C<< equal >> call.
357              
358             =cut
359              
360             sub error {
361 0     0 1   my $self = shift;
362 0           return $self->{error};
363             }
364              
365             1;
366              
367             __END__
368              
369             =back
370              
371             =head1 BUGS
372              
373             Please report any bugs or feature requests to through the GitHub web interface
374             at L<https://github.com/kasei/perlrdf/issues>.
375              
376             =head1 AUTHOR
377              
378             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
379              
380             =head1 COPYRIGHT
381              
382             Copyright (c) 2006-2012 Gregory Todd Williams. This
383             program is free software; you can redistribute it and/or modify it under
384             the same terms as Perl itself.
385              
386             =cut