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.017
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   437 use strict;
  68         149  
  68         1736  
35 68     68   330 use warnings;
  68         152  
  68         1785  
36 68     68   324 no warnings 'redefine';
  68         152  
  68         2086  
37              
38 68     68   30494 use Algorithm::Combinatorics qw(permutations);
  68         183071  
  68         5604  
39              
40             our ($VERSION, $debug, $AUTOLOAD);
41             BEGIN {
42 68     68   258 $debug = 0;
43 68         3627 $VERSION = '1.017';
44             }
45              
46             use overload
47 68         831 '==' => \&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   477 ;
  68         150  
54              
55             sub _eq {
56 2     2   223 my ($x, $y) = @_;
57 2         8 return $x->equals($y);
58             }
59              
60             sub _le {
61 4     4   9 my ($x, $y) = @_;
62 4         13 return $x->is_subgraph_of($y);
63             }
64              
65             sub _ge {
66 2     2   10 return _le(@_[1,0]);
67             }
68              
69             sub _lt {
70 4     4   11 my ($x, $y) = @_;
71             # Test::More::diag(sprintf('%s // %s', ref($x), ref($y)));
72 4   66     26 return ($x->size < $y->size) && ($x->is_subgraph_of($y));
73             }
74              
75             sub _gt {
76 2     2   7 return _lt(@_[1,0]);
77             }
78              
79 68     68   42368 use Data::Dumper;
  68         242272  
  68         3612  
80 68     68   474 use Log::Log4perl;
  68         144  
  68         521  
81 68     68   4017 use Scalar::Util qw(blessed);
  68         171  
  68         2764  
82 68     68   22536 use RDF::Trine::Node;
  68         246  
  68         3365  
83 68     68   25199 use RDF::Trine::Store;
  68         302  
  68         83385  
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 1299 my $class = shift;
95 33 100       149 unless (blessed($_[0])) {
96 2         23 throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::new must be called with a Model or Iterator argument";
97             }
98            
99 31         70 my %data;
100 31 100       282 if ($_[0]->isa('RDF::Trine::Iterator::Graph')) {
    100          
101 2         19 my $iter = shift;
102 2         22 my $model = RDF::Trine::Model->new( RDF::Trine::Store->temporary_store() );
103 2         12 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         78 $data{ model } = shift;
109             } else {
110 1         7 throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::new must be called with a Model or Iterator argument";
111             }
112            
113 30         122 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 2500 my $self = shift;
125 11         23 my $graph = shift;
126 11         62 undef($self->{error});
127 11 100       45 return $self->_check_equality($graph) ? 1 : 0;
128             }
129              
130             sub _check_equality {
131 11     11   25 my $self = shift;
132 11         20 my $graph = shift;
133 11 100 100     77 unless (blessed($graph) and $graph->isa('RDF::Trine::Graph')) {
134 2         6 $self->{error} = "RDF::Trine::Graph::equals must be called with a Graph argument";
135 2         17 throw RDF::Trine::Error::MethodInvocationError -text => $self->{error};
136             }
137            
138 9         30 my @graphs = ($self, $graph);
139 9         33 my ($ba, $nba) = $self->split_blank_statements;
140 9         30 my ($bb, $nbb) = $graph->split_blank_statements;
141 9 100       35 if (scalar(@$nba) != scalar(@$nbb)) {
142 1         4 my $nbac = scalar(@$nba);
143 1         2 my $nbbc = scalar(@$nbb);
144 1         5 $self->{error} = "count of non-blank statements didn't match ($nbac != $nbbc)";
145 1         12 return 0;
146             }
147 8         21 my $bac = scalar(@$ba);
148 8         17 my $bbc = scalar(@$bb);
149 8 100       23 if ($bac != $bbc) {
150 1         6 $self->{error} = "count of blank statements didn't match ($bac != $bbc)";
151 1         10 return 0;
152             }
153            
154 7         20 for ($nba, $nbb) {
155 14         33 @$_ = sort map { $_->as_string } @$_;
  12         33  
156             }
157            
158 7         14 foreach my $i (0 .. $#{ $nba }) {
  7         26  
159 6 100       24 unless ($nba->[$i] eq $nbb->[$i]) {
160 1         8 $self->{error} = "non-blank triples don't match: " . Dumper($nba->[$i], $nbb->[$i]);
161 1         105 return 0;
162             }
163             }
164            
165 6         24 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 2512 my $self = shift;
177 15         26 my $graph = shift;
178 15         71 undef($self->{error});
179 15 100       46 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   29 my $self = shift;
202 15         25 my $graph = shift;
203 15 100 100     91 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         37 my @graphs = ($self, $graph);
208 13         39 my ($ba, $nba) = $self->split_blank_statements;
209 13         39 my ($bb, $nbb) = $graph->split_blank_statements;
210            
211 13 50       60 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         34 my %NBB = map { $_->as_string => 1 } @$nbb;
  33         90  
220            
221 13         38 foreach my $st (@$nba) {
222 11 100       30 unless ($NBB{ $st->as_string }) {
223 1         8 return 0;
224             }
225             }
226            
227 12         39 return _find_mapping($self, $ba, $bb);
228             }
229              
230             sub _find_mapping {
231 18     18   46 my ($self, $ba, $bb) = @_;
232              
233 18 100       57 if (scalar(@$ba) == 0) {
234 2         26 return {};
235             }
236            
237 16         31 my %blank_ids_a;
238 16         33 foreach my $st (@$ba) {
239 26         72 foreach my $n (grep { $_->isa('RDF::Trine::Node::Blank') } $st->nodes) {
  78         239  
240 30         77 $blank_ids_a{ $n->blank_identifier }++;
241             }
242             }
243              
244 16         34 my %blank_ids_b;
245 16         36 foreach my $st (@$bb) {
246 40         113 foreach my $n (grep { $_->isa('RDF::Trine::Node::Blank') } $st->nodes) {
  120         333  
247 44         109 $blank_ids_b{ $n->blank_identifier }++;
248             }
249             }
250            
251 16         33 my %bb_master = map { $_->as_string => 1 } @$bb;
  40         96  
252            
253 16         65 my @ka = keys %blank_ids_a;
254 16         43 my @kb = keys %blank_ids_b;
255 16         82 my $kbp = permutations( \@kb );
256 16         845 my $count = 0;
257 16         55 MAPPING: while (my $mapping = $kbp->next) {
258 28         418 my %mapping;
259 28         74 @mapping{ @ka } = @$mapping;
260 28 50       71 warn "trying mapping: " . Dumper(\%mapping) if ($debug);
261            
262 28         83 my %bb = %bb_master;
263 28         76 foreach my $st (@$ba) {
264 42         66 my @nodes;
265 42         124 foreach my $method ($st->node_names) {
266 126         325 my $n = $st->$method();
267 126 100       404 if ($n->isa('RDF::Trine::Node::Blank')) {
268 53         130 my $id = $mapping{ $n->blank_identifier };
269 53 50       121 warn "mapping " . $n->blank_identifier . " to $id\n" if ($debug);
270 53         143 push(@nodes, RDF::Trine::Node::Blank->new( $id ));
271             } else {
272 73         140 push(@nodes, $n);
273             }
274             }
275 42         83 my $class = ref($st);
276 42         118 my $mapped_st = $class->new( @nodes )->as_string;
277 42 50       122 warn "checking for '$mapped_st' in " . Dumper(\%bb) if ($debug);
278 42 100       106 if ($bb{ $mapped_st }) {
279 26         82 delete $bb{ $mapped_st };
280             } else {
281 16         91 next MAPPING;
282             }
283             }
284 12 50       35 $self->{error} = "found mapping: " . Dumper(\%mapping) if ($debug);
285 12         222 return \%mapping;
286             }
287            
288 4         43 $self->{error} = "didn't find blank node mapping\n";
289 4         50 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 78 my $self = shift;
301 44         255 my $iter = $self->get_statements;
302 44         98 my (@blanks, @nonblanks);
303 44         149 while (my $st = $iter->next) {
304 135 100       321 if ($st->has_blanks) {
305 75         252 push(@blanks, $st);
306             } else {
307 60         185 push(@nonblanks, $st);
308             }
309             }
310 44         396 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 6536 my ($proto, $queried) = @_;
331 46 50       142 $proto = ref($proto) if ref($proto);
332 46   33     281 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 16 my ($proto, $queried) = @_;
341 1 50       5 $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   6982 my $self = shift;
347 82 100       399 return if $AUTOLOAD =~ /::DESTROY$/;
348 52         309 $AUTOLOAD =~ s/^(.+)::([^:]+)$/$2/;
349 52         246 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