File Coverage

blib/lib/XML/SemanticCompare.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             #-----------------------------------------------------------------
2             # XML::SemanticCompare
3             # Author: Edward Kawas ,
4             # For copyright and disclaimer see below.
5             #
6             # $Id: SemanticCompare.pm,v 1.1 2009-12-01 21:12:28 ubuntu Exp $
7             #-----------------------------------------------------------------
8             package XML::SemanticCompare;
9 1     1   519104 use strict;
  1         3  
  1         43  
10 1     1   5 use Carp;
  1         1  
  1         68  
11 1     1   7 use vars qw /$VERSION/;
  1         3  
  1         87  
12             $VERSION = sprintf "%d.%02d", q$Revision: 1.1 $ =~ /: (\d+)\.(\d+)/;
13 1     1   6 use vars qw($AUTOLOAD);
  1         2  
  1         32  
14              
15             #-----------------------------------------------------------------
16             # load all modules needed
17             #-----------------------------------------------------------------
18 1     1   522 use XML::Simple;
  0            
  0            
19             use XML::LibXML;
20             use XML::SemanticCompare::SAX;
21              
22             use Data::Dumper;
23              
24             =head1 NAME
25              
26             XML::SemanticCompare - compare 2 XML trees semantically
27              
28             =head1 SYNOPSIS
29              
30             use XML::SemanticCompare;
31             my $x = XML::SemanticCompare->new;
32              
33             # compare 2 different files
34             my $isSame = $x->compare($control_xml, $test_xml);
35             # are they the same
36             print "XML matches!\n"
37             if $isSame;
38             print "XML files are semantically different!\n"
39             unless $isSame;
40              
41             # get the diffs and print them out
42             my $diffs_arrayref = $x->diff( $control_xml, $test_xml );
43             print "Diff: $_\n" foreach (@$diffs_arrayref);
44              
45             # test xpath statement against XML
46             my $success = $x->test_xpath($xpath, $test_xml);
47             print "xpath success!\n" if $success;
48              
49             =head1 DESCRIPTION
50              
51             This module is used for semantically comparing XML documents.
52              
53             =cut
54              
55             =head1 AUTHORS
56              
57             Edward Kawas (edward.kawas+xml-semantic-compare@gmail.com)
58              
59             =cut
60              
61             #-----------------------------------------------------------------
62             # AUTOLOAD
63             #-----------------------------------------------------------------
64             sub AUTOLOAD {
65             my $self = shift;
66             my $type = ref($self)
67             or croak("$self is not an object");
68             my $name = $AUTOLOAD;
69             $name =~ s/.*://; # strip fully-qualified portion
70             unless ( exists $self->{_permitted}->{$name} ) {
71             croak("Can't access '$name' field in class $type");
72             }
73             my $is_func = $self->{_permitted}->{$name}[1] =~ m/subroutine/i;
74             unless ($is_func) {
75             if (@_) {
76             my $val = shift;
77             $val = $val || "";
78             return $self->{$name} = $val
79             if $self->{_permitted}->{$name}[1] =~ m/write/i;
80             croak("Can't write to '$name' field in class $type");
81             } else {
82             return $self->{$name}
83             if $self->{_permitted}->{$name}[1] =~ m/read/i;
84             croak("Can't read '$name' field in class $type");
85             }
86             }
87              
88             # call a function
89             if ($is_func) {
90             if (@_) {
91              
92             # parameterized call
93             my $x = $self->{_permitted}->{$name}[0];
94             return $self->$x(@_);
95             } else {
96              
97             # un-parameterized call
98             my $x = $self->{_permitted}->{$name}[0];
99             return $self->$x();
100             }
101             }
102             }
103              
104             #-----------------------------------------------------------------
105             # new
106             #-----------------------------------------------------------------
107             sub new {
108             my ( $class, %options ) = @_;
109              
110             # permitted fields
111             my %fields = (
112              
113             # attribute => [default, accessibility],
114             trim => [ 1, 'read/write' ],
115             use_attr => [ 1, 'read/write' ],
116             compare => [ "_test_xml", 'subroutine' ],
117             diff => [ "_get_xml_differences", 'subroutine' ],
118             test_xpath => [ "_test_xpath_statement", 'subroutine' ],
119             );
120              
121             # create an object
122             my $self = { _permitted => \%fields };
123              
124             # set user values if they exist
125             $self->{trim} = $options{trim} || '1';
126             $self->{use_attr} = $options{use_attr} || '1';
127             bless $self, $class;
128             return $self;
129             }
130              
131             #-----------------------------------------------------------------
132             # _test_xml: semantically compare $control_xml to $xml
133             #-----------------------------------------------------------------
134             sub _test_xml {
135             my ( $self, $control_xml, $xml ) = @_;
136             return undef unless $control_xml;
137             return undef unless $xml;
138             return undef if $control_xml =~ m//g;
139             return undef if $xml =~ m//g;
140              
141             # check the root element name first
142             # this isnt very efficient, but until someone gives a better way ...
143             my $parser = XML::LibXML->new();
144             my $cont_ele = undef;
145             my $test_ele = undef;
146              
147             # try parsing a string or a file
148             eval { $cont_ele = $parser->parse_string($control_xml); };
149             eval { $cont_ele = $parser->parse_file($control_xml); } if $@;
150             return undef if $@;
151             eval { $test_ele = $parser->parse_string($xml); };
152             eval { $test_ele = $parser->parse_file($xml); } if $@;
153             return undef if $@;
154             $cont_ele = $cont_ele->getDocumentElement;
155             $test_ele = $test_ele->getDocumentElement;
156             return undef
157             unless $cont_ele->localname eq $test_ele->localname
158             and $cont_ele->namespaceURI() eq $test_ele->namespaceURI();
159              
160             # free memory
161             $parser = undef;
162             $cont_ele = undef;
163             $test_ele = undef;
164              
165             # done checking the root element
166             # create object with attributes
167             my $xml_simple = new XML::Simple(
168             ForceArray => 1,
169             ForceContent => 1,
170              
171             # SuppressEmpty => 1,
172             keyattr => [],
173             ) if $self->use_attr;
174              
175             # or create it without attributes
176             $xml_simple = new XML::Simple(
177             ForceArray => 1,
178             ForceContent => 1,
179              
180             # SuppressEmpty => 1,
181             NoAttr => 1,
182             keyattr => [],
183             ) unless $self->use_attr;
184              
185             # read both XML files into a HASH
186             my $control = undef;
187             my $test = undef;
188              
189             # parse the control doc
190             eval { $control = $xml_simple->XMLin($control_xml); };
191              
192             # check for invalid XML
193             return undef if $@;
194              
195             # parse the test doc
196             eval { $test = $xml_simple->XMLin($xml); };
197              
198             # check for invalid XML
199             return undef if $@;
200             return $self->_compare_current_level( $control, $test, (), () );
201             }
202              
203             #-----------------------------------------------------------------
204             # _compare_current_level:
205             # compares current level of data structures that represent XML
206             # documents.
207             # If the current level and all child levels match, a true value
208             # is returned. Otherwise, undef is returned.
209             #-----------------------------------------------------------------
210             sub _compare_current_level {
211              
212             # $control is current level in hash
213             # x_ns are the prefixes that we use
214             my ( $self, $control, $test, $control_ns, $test_ns ) = @_;
215              
216             # if either hash is missing they arent equal
217             return undef unless $control;
218             return undef unless $test;
219              
220             # get the namespace prefix and uris at the current level
221             # for each doc and remove from current level of hash
222             for my $key ( keys %$control ) {
223             next unless $key =~ m/^xmlns[:]?/;
224              
225             #next unless $key =~ m|^{http://www\.w3\.org/2000/xmlns/}[\w]*$|;
226             $control_ns->{''} = $control->{$key} if $key eq 'xmlns';
227             $control_ns->{$1} = $control->{$key} if $key =~ m/xmlns\:(.*)$/g;
228             delete $control->{$key} if ref( $control->{$key} ) ne 'ARRAY';
229             }
230             for my $key ( keys %$test ) {
231             next unless $key =~ m/^xmlns[:]?/;
232              
233             #next unless $key =~ m|^{http://www\.w3\.org/2000/xmlns/}[\w]*$|;
234             $test_ns->{''} = $test->{$key} if $key eq 'xmlns';
235             $test_ns->{$1} = $test->{$key} if $key =~ m/xmlns\:(.*)$/g;
236             delete $test->{$key} if ref( $test->{$key} ) ne 'ARRAY';
237             }
238              
239             # compare current level number of keys
240             return undef unless ( keys %$control ) == ( keys %$test );
241              
242             # number of keys are equal, so start comparing!
243             my $matching_nodes = 0;
244             for my $key ( keys %$control ) {
245             my $success = 1;
246             for my $test_key ( keys %$test ) {
247              
248             # does the key exist?
249             # 'content' is a special case ... because its text content for a node
250             if (
251             ( $key eq $test_key and $key eq 'content' )
252             or ( $self->_get_prefixed_key( $test_key, $test_ns ) eq
253             $self->_get_prefixed_key( $key, $control_ns )
254             and $self->_get_prefixed_key( $key, $control_ns ) )
255             )
256             {
257              
258             # are we dealing with scalar values now or more nesting?
259             if ( ref( $control->{$key} ) eq 'ARRAY' ) {
260              
261             # both items should be an array
262             next unless ref( $test->{$test_key} ) eq 'ARRAY';
263              
264             # array sizes should match here ...
265             next
266             unless @{ $control->{$key} } == @{ $test->{$test_key} };
267              
268             # more nesting try matching child nodes
269             my $child_matches = 0;
270             foreach my $child ( @{ $control->{$key} } ) {
271             my $matched = undef;
272             foreach my $test_child ( @{ $test->{$test_key} } ) {
273             $matched =
274             $self->_compare_current_level( $child,
275             $test_child, $control_ns, $test_ns );
276             $child_matches++ if $matched;
277             last if $matched;
278             } # end inner foreach
279             $matching_nodes++
280             if @{ $control->{$key} } == $child_matches;
281             }
282             } else {
283              
284             # compare scalar values now
285             # we dont care about whitespace, so we need to trim the text
286             my $c_text = $self->_clear_whitespace( $control->{$key} );
287             my $t_text = $self->_clear_whitespace( $test->{$test_key} );
288             $matching_nodes++ if $c_text eq $t_text;
289             last if $c_text eq $t_text;
290             }
291             }
292             } #end inner for
293             }
294              
295             # no differences found!
296             return undef unless $matching_nodes == ( keys %$control );
297             return 1;
298             }
299              
300             #-----------------------------------------------------------------
301             # _clear_whitespace: a whitespace trim function
302             #-----------------------------------------------------------------
303             sub _clear_whitespace {
304             my ( $self, $text ) = @_;
305             return $text unless $self->trim;
306             $text =~ s/^\s+//;
307             $text =~ s/\s+$//;
308             return $text;
309             }
310              
311             #-----------------------------------------------------------------
312             # _get_prefixed_key:
313             # goes through and tries to determine what the namespace URI
314             # is for a prefix.
315             # Once a URI is found, the prefix is swapped with URI and
316             # returned.
317             #-----------------------------------------------------------------
318             sub _get_prefixed_key {
319             my ( $self, $key, $ns_hash ) = @_;
320             my $prefixed_key = $key;
321             my $prefix = $1 if $key =~ m/^([\w]+)\:.*/;
322             $prefixed_key =~ s/$prefix/$ns_hash->{$prefix}/
323             if $prefix and $ns_hash->{$prefix};
324              
325             # check for default xmlns
326             $prefix = $prefix || '';
327             $prefixed_key = $ns_hash->{$prefix} . ":" . $key
328             if not $prefix and defined $ns_hash->{$prefix};
329             return $prefixed_key;
330             }
331              
332             #-----------------------------------------------------------------
333             # _test_xpath_statement: apply $xpath to $xml
334             #-----------------------------------------------------------------
335             sub _test_xpath_statement {
336             my ( $self, $xpath, $xml ) = @_;
337              
338             # no xpath expression, nothing to test
339             return undef if $xpath =~ m//g;
340              
341             # empty xml, nothing to test
342             return undef if $xml =~ m//g;
343              
344             #instantiate a parser
345             my $parser = XML::LibXML->new();
346             my $tree = undef;
347              
348             # try parsing a string or a file
349             eval { $tree = $parser->parse_string($xml); };
350             eval { $tree = $parser->parse_file($xml); } if $@;
351             return undef if $@;
352             my $root = $tree->getDocumentElement;
353              
354             # evaluate the xpath statement
355             my $results = undef;
356             eval { $results = $root->find($xpath); };
357             return undef if $@;
358              
359             # no results?
360             return undef unless $results;
361              
362             # got some hits!
363             return 1;
364             }
365              
366             #-----------------------------------------------------------------
367             # _get_xml_differences:
368             # get the differences between $xml and expected xml
369             # and return them
370             #-----------------------------------------------------------------
371             sub _get_xml_differences {
372             my ( $self, $control_xml, $test_xml ) = @_;
373             my @diffs;
374              
375             # create a parser
376             my $parser = new XML::SemanticCompare::SAX();
377            
378             # parse a file $xml is an array of strings representing the XML tree
379             my $xml = undef;
380             eval {$xml = $parser->parse ( method => 'string', data => $control_xml );};
381             eval {$xml = $parser->parse ( method => 'file', data => $control_xml );} unless $xml;
382             #my %control = map {($_, 1)} @$xml;
383             my $xml2 = undef;
384             eval {$xml2 = $parser->parse ( method => 'string', data => $test_xml );};
385             eval {$xml2 = $parser->parse ( method => 'file', data => $test_xml );} unless $xml2;
386            
387             foreach my $i ( 0 .. scalar(@$xml)-1) {
388             next unless $xml->[$i];
389             foreach my $j (0 .. scalar(@$xml2)-1) {
390             next unless $xml2->[$i];
391             if ($xml->[$i] eq $xml2->[$j]) {
392             delete $xml->[$i];
393             delete $xml2->[$j];
394             last;
395             }
396             }
397             }
398             push @diffs, grep {defined $_} (@$xml, @$xml2);
399             # items left over ...
400             # grep {$control{$_} > 0} keys %control;
401             return \@diffs;
402             }
403              
404             sub DESTROY { }
405             1;
406             __END__