File Coverage

blib/lib/XML/Compare.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             ## ----------------------------------------------------------------------------
2             # Copyright (C) 2009 NZ Registry Services
3             ## ----------------------------------------------------------------------------
4             package XML::Compare;
5              
6 6     6   185827 use XML::LibXML;
  0            
  0            
7             use Any::Moose;
8              
9             our $VERSION = '0.04';
10             our $VERBOSE = $ENV{XML_COMPARE_VERBOSE} || 0;
11              
12             my $PARSER = XML::LibXML->new();
13              
14             my $has = {
15             localname => {
16             # not Comment, CDATASection
17             'XML::LibXML::Attr' => 1,
18             'XML::LibXML::Element' => 1,
19             },
20             namespaceURI => {
21             # not Comment, Text, CDATASection
22             'XML::LibXML::Attr' => 1,
23             'XML::LibXML::Element' => 1,
24             },
25             attributes => {
26             # not Attr, Comment, CDATASection
27             'XML::LibXML::Element' => 1,
28             },
29             value => {
30             # not Element, Comment, CDATASection
31             'XML::LibXML::Attr' => 1,
32             'XML::LibXML::Comment' => 1,
33             },
34             data => {
35             # not Element, Attr
36             'XML::LibXML::CDATASection' => 1,
37             'XML::LibXML::Comment' => 1,
38             'XML::LibXML::Text' => 1,
39             },
40             };
41              
42             has 'namespace_strict' =>
43             is => "rw",
44             isa => "Bool",
45             default => 0,
46             ;
47              
48             has 'error' =>
49             is => "rw",
50             isa => "Str",
51             clearer => "_clear_error",
52             ;
53              
54             sub _self {
55             my $args = shift;
56             if ( @$args == 3 ) {
57             shift @$args;
58             }
59             else {
60             __PACKAGE__->new();
61             }
62             }
63              
64             # acts almost like an assertion (either returns true or throws an exception)
65             sub same {
66             my $self = _self(\@_);
67             my ($xml1, $xml2) = @_;
68             # either throws an exception, or returns true
69             return $self->_compare($xml1, $xml2);;
70             }
71              
72             sub is_same {
73             my $self = _self(\@_);
74             my ($xml1, $xml2) = @_;
75             # catch the exception and return true or false
76             $self->_clear_error;
77             eval { $self->same($xml1, $xml2); };
78             if ( $@ ) {
79             $self->error($@);
80             return 0;
81             }
82             return 1;
83             }
84              
85             sub is_different {
86             my $self = _self(\@_);
87             my ($xml1, $xml2) = @_;
88             return !$self->is_same($xml1, $xml2);
89             }
90              
91             # private functions
92             sub _xpath {
93             my $l = shift;
94             "/".join("/",@$l);
95             }
96              
97             sub _die {
98             my ($l, $fmt, @args) = @_;
99             my $msg;
100             if ( @args ) {
101             $msg = sprintf $fmt, @args;
102             }
103             else {
104             $msg = $fmt;
105             }
106             die("[at "._xpath($l)."]: ".$msg);
107             }
108              
109             sub _compare {
110             my $self = shift;
111             my ($xml1, $xml2) = (@_);
112             if ( $VERBOSE ) {
113             print '-' x 79, "\n";
114             print $xml1 . ($xml1 =~ /\n\Z/ ? "" : "\n");
115             print '-' x 79, "\n";
116             print $xml2 . ($xml2 =~ /\n\Z/ ? "" : "\n");
117             print '-' x 79, "\n";
118             }
119              
120             my $parser = XML::LibXML->new();
121             my $doc1 = $parser->parse_string( $xml1 );
122             my $doc2 = $parser->parse_string( $xml2 );
123             return $self->_are_docs_same($doc1, $doc2);
124             }
125              
126             sub _are_docs_same {
127             my $self = shift;
128             my ($doc1, $doc2) = @_;
129             my $ignore = $self->ignore;
130             if ( $ignore and @$ignore ) {
131             my $in = {};
132             for my $doc ( map { $_->documentElement } $doc1, $doc2 ) {
133             my $xpc;
134             if ( my $ix = $self->ignore_xmlns ) {
135             $xpc = XML::LibXML::XPathContext->new($doc);
136             $xpc->registerNs($_ => $ix->{$_})
137             for keys %$ix;
138             }
139             else {
140             $xpc = $doc;
141             }
142             for my $ignore_xpath ( @$ignore ) {
143             $in->{$_->nodePath}=undef
144             for $xpc->findnodes( $ignore_xpath );
145             }
146             }
147             $self->_ignore_nodes($in);
148             }
149             else {
150             $self->_ignore_nothing;
151             }
152             return $self->_are_nodes_same(
153             [ $doc1->documentElement->nodeName ],
154             $doc1->documentElement,
155             $doc2->documentElement,
156             );
157             }
158              
159             has 'ignore' =>
160             is => "rw",
161             isa => "ArrayRef[Str]",
162             ;
163              
164             has 'ignore_xmlns' =>
165             is => "rw",
166             isa => "HashRef[Str]",
167             ;
168              
169             has '_ignore_nodes' =>
170             is => "rw",
171             isa => "HashRef[Undef]",
172             clearer => "_ignore_nothing",
173             ;
174              
175             sub _are_nodes_same {
176             my $self = shift;
177             my ($l, $node1, $node2) = @_;
178             _msg($l, "\\ got (" . ref($node1) . ", " . ref($node2) . ")");
179              
180             # firstly, check that the node types are the same
181             my $nt1 = $node1->nodeType();
182             my $nt2 = $node2->nodeType();
183             if ( $nt1 eq $nt2 ) {
184             _same($l, "nodeType=$nt1");
185             }
186             else {
187             _outit($l, 'node types are different', $nt1, $nt2);
188             _die $l, 'node types are different (%s, %s)', $nt1, $nt2;
189             }
190              
191             # if these nodes are Text, compare the contents
192             if ( $has->{data}{ref $node1} ) {
193             my $data1 = $node1->data();
194             my $data2 = $node2->data();
195             # _msg($l, ": data ($data1, $data2)");
196             if ( $data1 eq $data2 ) {
197             _same($l, "data");
198             }
199             else {
200             _outit($l, 'data differs', $data1, $data2);
201             _die $l, 'data differs: (%s, %s)', $data1, $data2;
202             }
203             }
204              
205             # if these nodes are Attr, compare the contents
206             if ( $has->{value}{ref $node1} ) {
207             my $val1 = $node1->getValue();
208             my $val2 = $node2->getValue();
209             # _msg($l, ": val ($val1, $val2)");
210             if ( $val1 eq $val2 ) {
211             _same($l, "value");
212             }
213             else {
214             _outit($l, 'attr node values differs', $val1, $val2);
215             _die $l, "attr node values differs (%s, %s)", $val1, $val2
216             }
217             }
218              
219             # check that the nodes are the same name (localname())
220             if ( $has->{localname}{ref $node1} ) {
221             my $ln1 = $node1->localname();
222             my $ln2 = $node2->localname();
223             if ( $ln1 eq $ln2 ) {
224             _same($l, 'localname');
225             }
226             else {
227             _outit($l, 'node names are different', $ln1, $ln2);
228             _die $l, 'node names are different: ', $ln1, $ln2;
229             }
230             }
231              
232             # check that the nodes are the same namespace
233             if ( $has->{namespaceURI}{ref $node1} ) {
234             my $ns1 = $node1->namespaceURI();
235             my $ns2 = $node2->namespaceURI();
236             # _msg($l, ": namespaceURI ($ns1, $ns2)");
237             if ( defined $ns1 and defined $ns2 ) {
238             if ( $ns1 eq $ns2 ) {
239             _same($l, 'namespaceURI');
240             }
241             else {
242             _outit($l, 'namespaceURIs are different', $node1->namespaceURI(), $node2->namespaceURI());
243             _die $l, 'namespaceURIs are different: (%s, %s)', $ns1, $ns2;
244             }
245             }
246             elsif ( !defined $ns1 and !defined $ns2 ) {
247             _same($l, 'namespaceURI (not defined for either node)');
248             }
249             else {
250             if ( $self->namespace_strict or defined $ns1 ) {
251             _outit($l, 'namespaceURIs are defined/not defined', $ns1, $ns2);
252             _die $l, 'namespaceURIs are defined/not defined: (%s, %s)', ($ns1 || '[undef]'), ($ns2 || '[undef]');
253             }
254             }
255             }
256              
257             # check the attribute list is the same length
258             if ( $has->{attributes}{ref $node1} ) {
259              
260             my $in = $self->_ignore_nodes;
261             # get just the Attrs and sort them by namespaceURI:localname
262             my @attr1 = sort { _fullname($a) cmp _fullname($b) }
263             grep { !$in or !exists $in->{$_->nodePath} }
264             grep { defined and $_->isa('XML::LibXML::Attr') }
265             $node1->attributes();
266              
267             my @attr2 = sort { _fullname($a) cmp _fullname($b) }
268             grep { !$in or !exists $in->{$_->nodePath} }
269             grep { defined and $_->isa('XML::LibXML::Attr') }
270             $node2->attributes();
271              
272             if ( scalar @attr1 == scalar @attr2 ) {
273             _same($l, 'attribute length (' . (scalar @attr1) . ')');
274             }
275             else {
276             _die $l, 'attribute list lengths differ: (%d, %d)', scalar @attr1, scalar @attr2;
277             }
278              
279             # for each attribute, check they are all the same
280             my $total_attrs = scalar @attr1;
281             for (my $i = 0; $i < scalar @attr1; $i++ ) {
282             # recurse down (either an exception will be thrown, or all are correct
283             $self->_are_nodes_same( [@$l,'@'.$attr1[$i]->name], $attr1[$i], $attr2[$i] );
284             }
285             }
286              
287             my $in = $self->_ignore_nodes;
288              
289             # don't need to compare or care about Comments
290             my @nodes1 = grep { !$in or !exists $in->{$_->nodePath} }
291             grep { ! $_->isa('XML::LibXML::Comment') and
292             !($_->isa("XML::LibXML::Text") && ($_->data =~ /\A\s*\Z/))
293             }
294             $node1->childNodes();
295              
296             my @nodes2 = grep { !$in or !exists $in->{$_->nodePath} }
297             grep { ! $_->isa('XML::LibXML::Comment') and
298             !($_->isa("XML::LibXML::Text") && ($_->data =~ /\A\s*\Z/))
299             } $node2->childNodes();
300              
301             # firstly, convert all CData nodes to Text Nodes
302             @nodes1 = _convert_cdata_to_text( @nodes1 );
303             @nodes2 = _convert_cdata_to_text( @nodes2 );
304              
305             # append all the consecutive Text nodes
306             @nodes1 = _squash_text_nodes( @nodes1 );
307             @nodes2 = _squash_text_nodes( @nodes2 );
308              
309             # check that the nodes contain the same number of children
310             if ( @nodes1 != @nodes2 ) {
311             _die $l, 'different number of child nodes: (%d, %d)', scalar @nodes1, scalar @nodes2;
312             }
313              
314             # foreach of it's children, compare them
315             my $total_nodes = scalar @nodes1;
316             for (my $i = 0; $i < $total_nodes; $i++ ) {
317             # recurse down (either an exception will be thrown, or all are correct
318             my $xpath_nodeName;
319             my $nn = $nodes1[$i]->nodeName;
320             if ( grep { $_->nodeName eq $nn }
321             @nodes1[0..$i-1, $i+1..$#nodes1] ) {
322             $nn .= "[position()=".($i+1)."]";
323             }
324             $nn =~ s{#text}{text()};
325             $self->_are_nodes_same( [@$l,$nn], $nodes1[$i], $nodes2[$i] );
326             }
327              
328             _msg($l, '/');
329             return 1;
330             }
331              
332             # takes an array of nodes and converts all the CDATASection nodes into Text nodes
333             sub _convert_cdata_to_text {
334             my @nodes = @_;
335             my @new;
336             foreach my $n ( @nodes ) {
337             if ( ref $n eq 'XML::LibXML::CDATASection' ) {
338             $n = XML::LibXML::Text->new( $n->data() );
339             }
340             push @new, $n;
341             }
342             return @new;
343             }
344              
345             # takes an array of nodes and concatenates all the Text nodes together
346             sub _squash_text_nodes {
347             my @nodes = @_;
348             my @new;
349             my $last_type = '';
350             foreach my $n ( @nodes ) {
351             if ( $last_type eq 'XML::LibXML::Text' and ref $n eq 'XML::LibXML::Text' ) {
352             $n = XML::LibXML::Text->new( $new[-1]->data() . $n->data() );
353             $new[-1] = $n;
354             }
355             else {
356             push @new, $n;
357             }
358             $last_type = ref $n;
359             }
360             return @new;
361             }
362              
363             sub _fullname {
364             my ($node) = @_;
365             my $name = '';
366             $name .= $node->namespaceURI() . ':' if $node->namespaceURI();
367             $name .= $node->localname();
368             # print "name=$name\n";
369             return $name;
370             }
371              
372             sub _same {
373             my ($l, $msg) = @_;
374             return unless $VERBOSE;
375             print '' . (' ' x (@$l+1)) . "= $msg\n";
376             }
377              
378             sub _msg {
379             my ($l, $msg) = @_;
380             return unless $VERBOSE;
381             print ' ' . (' ' x (@$l)) ._xpath($l). " $msg\n";
382             }
383              
384             sub _outit {
385             my ($l, $msg, $v1, $v2) = @_;
386             return unless $VERBOSE;
387             print '' . (' ' x @$l) . "! " ._xpath($l)." $msg:\n";
388             print '' . (' ' x @$l) . '. ' . ($v1 || '[undef]') . "\n";
389             print '' . (' ' x @$l) . '. ' . ($v2 || '[undef]') . "\n";
390             }
391              
392             1;
393             __END__