File Coverage

lib/XML/LibXML/Simple.pm
Criterion Covered Total %
statement 231 248 93.1
branch 149 194 76.8
condition 73 106 68.8
subroutine 22 24 91.6
pod 2 7 28.5
total 477 579 82.3


line stmt bran cond sub pod time code
1             # Copyrights 2008-2020 by [Mark Overmeer ].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution XML-LibXML-Simple. Meta-POD processed
6             # with OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package XML::LibXML::Simple;
10 2     2   57989 use vars '$VERSION';
  2         4  
  2         124  
11             $VERSION = '1.01';
12              
13 2     2   13 use base 'Exporter';
  2         4  
  2         216  
14              
15 2     2   12 use strict;
  2         3  
  2         61  
16 2     2   12 use warnings;
  2         3  
  2         129  
17              
18             our @EXPORT = qw(XMLin);
19             our @EXPORT_OK = qw(xml_in);
20              
21 2     2   653 use XML::LibXML ();
  2         41032  
  2         51  
22 2     2   11 use File::Basename qw/fileparse/;
  2         3  
  2         151  
23 2     2   14 use File::Spec ();
  2         5  
  2         36  
24 2     2   10 use Carp;
  2         4  
  2         101  
25 2     2   10 use Scalar::Util qw/blessed/;
  2         4  
  2         84  
26              
27 2     2   9 use Data::Dumper; #to be removed
  2         5  
  2         6400  
28              
29              
30             my %known_opts = map +($_ => 1),
31             qw(keyattr keeproot forcecontent contentkey noattr searchpath
32             forcearray grouptags nsexpand normalisespace normalizespace
33             valueattr nsstrip parser parseropts hooknodes suppressempty);
34              
35             my @default_attributes = qw(name key id);
36             my $default_content_key = 'content';
37              
38             #-------------
39              
40             sub new(@)
41 84     84 1 175 { my $class = shift;
42 84         166 my $self = bless {}, $class;
43 84         204 my $opts = $self->{opts} = $self->_take_opts(@_);
44              
45             # parser object cannot be reused
46             !defined $opts->{parser}
47 84 50       195 or error __x"parser option for XMLin only";
48              
49 84         158 $self;
50             }
51              
52             #-------------
53              
54             sub XMLin
55 84 50 66 84 1 92339 { my $self = @_ > 1 && blessed $_[0] && $_[0]->isa(__PACKAGE__) ? shift
56             : __PACKAGE__->new;
57 84         132 my $target = shift;
58              
59 84         154 my $this = $self->_take_opts(@_);
60 82         206 my $opts = $self->_init($self->{opts}, $this);
61              
62 82 50       222 my $xml = $self->_get_xml($target, $opts)
63             or return;
64              
65 79 50       554 if(my $cb = $opts->{hooknodes})
66 0         0 { $self->{XCS_hooks} = $cb->($self, $xml);
67             }
68              
69 79         199 my $top = $self->collapse($xml, $opts);
70 79 100       166 if($opts->{keeproot})
71             { my $subtop
72 1 50 33     8 = $opts->{forcearray_always} && ref $top ne 'ARRAY' ? [$top] : $top;
73 1         12 $top = +{ $xml->localName => $subtop };
74             }
75              
76 79         228 $top;
77             }
78             *xml_in = \&XMLin;
79              
80             sub _get_xml($$)
81 82     82   155 { my ($self, $source, $opts) = @_;
82              
83 82 100       183 $source = $self->default_data_source($opts)
84             unless defined $source;
85              
86 82 100       157 $source = \*STDIN
87             if $source eq '-';
88              
89             my $parser = $opts->{parser}
90 82   33     230 || $self->_create_parser($opts->{parseropts});
91              
92             my $xml
93             = blessed $source &&
94             ( $source->isa('XML::LibXML::Document')
95             || $source->isa('XML::LibXML::Element' )) ? $source
96             : ref $source eq 'SCALAR' ? $parser->parse_string($$source)
97             : ref $source ? $parser->parse_fh($source)
98             : $source =~ m{^\s*\<.*?\>\s*$}s ? $parser->parse_string($source)
99             : $parser->parse_file
100 82 100 66     8552 ($self->find_xml_file($source, @{$opts->{searchpath}}));
  7 100       27  
    50          
    50          
101              
102 79 50       14096 $xml = $xml->documentElement
103             if $xml->isa('XML::LibXML::Document');
104              
105 79         629 $xml;
106             }
107              
108             sub _create_parser(@)
109 82     82   120 { my $self = shift;
110 82 50       235 my @popt = @_ != 1 ? @_ : ref $_[0] eq 'HASH' ? %{$_[0]} : @{$_[0]};
  82 50       181  
  0         0  
111              
112             XML::LibXML->new
113             ( line_numbers => 1
114             , no_network => 1
115             , expand_xinclude => 0
116             , expand_entities => 1
117             , load_ext_dtd => 0
118             , ext_ent_handler =>
119 0     0   0 sub { alert __x"parsing external entities disabled"; '' }
  0         0  
120             , @popt
121 82         453 );
122             }
123              
124             sub _take_opts(@)
125 168     168   236 { my $self = shift;
126            
127 168         232 my %opts;
128 168 100       368 @_ % 2==0
129             or die "ERROR: odd number of options.\n";
130              
131 167         323 while(@_)
132 119         234 { my ($key, $val) = (shift, shift);
133 119         192 my $lkey = lc $key;
134 119         249 $lkey =~ s/_//g;
135 119 100       503 $known_opts{$lkey} or croak "Unrecognised option: $key";
136 118         304 $opts{$lkey} = $val;
137             }
138              
139 166         368 \%opts;
140             }
141              
142             # Returns the name of the XML file to parse if no filename or XML string
143             # was provided explictly.
144              
145             sub default_data_source($)
146 1     1 0 4 { my ($self, $opts) = @_;
147              
148 1         83 my ($basename, $script_dir, $ext) = fileparse $0, qr[\.[^\.]+];
149              
150             # Add script directory to searchpath
151 1 50       7 unshift @{$opts->{searchpath}}, $script_dir
  1         4  
152             if $script_dir;
153              
154 1         4 "$basename.xml";
155             }
156              
157             sub _init($$)
158 82     82   157 { my ($self, $global, $this) = @_;
159 82         299 my %opt = (%$global, %$this);
160              
161 82 100       171 if(defined $opt{contentkey})
162 48         239 { $opt{collapseagain} = $opt{contentkey} =~ s/^\-// }
163 34         66 else { $opt{contentkey} = $default_content_key }
164              
165 82   100     462 $opt{normalisespace} ||= $opt{normalizespace} || 0;
      100        
166              
167 82   100     301 $opt{searchpath} ||= [];
168             ref $opt{searchpath} eq 'ARRAY'
169 82 100       211 or $opt{searchpath} = [ $opt{searchpath} ];
170              
171 82   100     225 my $fa = delete $opt{forcearray} || 0;
172 82         123 my (@fa_regex, %fa_elem);
173 82 100       152 if(ref $fa)
174 6 100       29 { foreach (ref $fa eq 'ARRAY' ? @$fa : $fa)
175 8 100       18 { if(ref $_ eq 'Regexp') { push @fa_regex, $_ }
  3         8  
176 5         15 else { $fa_elem{$_} = 1 }
177             }
178             }
179 76         121 else { $opt{forcearray_always} = $fa }
180 82         137 $opt{forcearray_regex} = \@fa_regex;
181 82         148 $opt{forcearray_elem} = \%fa_elem;
182              
183             # Special cleanup for {keyattr} which could be arrayref or hashref,
184             # which behave differently.
185              
186 82   100     222 my $ka = $opt{keyattr} || \@default_attributes;
187 82 100       177 $ka = [ $ka ] unless ref $ka;
188            
189 82 100       177 if(ref $ka eq 'ARRAY')
    50          
190 62 100       121 { if(@$ka) { $opt{keyattr} = $ka }
  60         122  
191 2         3 else { delete $opt{keyattr} }
192             }
193             elsif(ref $ka eq 'HASH')
194             { # Convert keyattr => { elem => '+attr' }
195             # to keyattr => { elem => [ 'attr', '+' ] }
196 20         30 my %at;
197 20         68 while(my($k,$v) = each %$ka)
198 23         112 { $v =~ /^(\+|-)?(.*)$/;
199 23   100     167 $at{$k} = [ $2, $1 || '' ];
200             }
201 20         46 $opt{keyattr} = \%at;
202             }
203              
204             # Special cleanup for {valueattr} which could be arrayref or hashref
205              
206 82   100     263 my $va = delete $opt{valueattr} || {};
207 82 100       187 $va = +{ map +($_ => 1), @$va } if ref $va eq 'ARRAY';
208 82         165 $opt{valueattrlist} = $va;
209              
210             # make sure there's nothing weird in {grouptags}
211              
212 82 50 66     218 !$opt{grouptags} || ref $opt{grouptags} eq 'HASH'
213             or croak "Illegal value for 'GroupTags' option -expected a hashref";
214              
215 82   50     301 $opt{parseropts} ||= {};
216              
217 82         187 \%opt;
218             }
219              
220             sub find_xml_file($@)
221 7     7 0 14 { my ($self, $file) = (shift, shift);
222 7 100       21 my @search_path = @_ ? @_ : '.';
223              
224 7         153 my ($filename, $filedir) = fileparse $file;
225              
226 7 100       110 if($filename eq $file)
    100          
227 4         8 { foreach my $path (@search_path)
228 6         60 { my $fullpath = File::Spec->catfile($path, $file);
229 6 100       200 return $fullpath if -e $fullpath;
230             }
231             }
232             elsif(-e $file) # Ignore searchpath if dir component
233 2         18 { return $file;
234             }
235              
236 3         13 local $" = ':';
237 3         44 die "data source $file not found in @search_path\n";
238             }
239              
240             sub _add_kv($$$$)
241 540     540   1045 { my ($d, $k, $v, $opts) = @_;
242              
243 540 100 66     2270 if(defined $d->{$k})
    100 66        
    100 100        
    100          
244             { # Combine duplicate attributes into arrayref if required
245 97 100       209 if(ref $d->{$k} eq 'ARRAY') { push @{$d->{$k}}, $v }
  46         61  
  46         118  
246 51         138 else { $d->{$k} = [ $d->{$k}, $v ] } }
247 2         3 elsif(ref $v eq 'ARRAY') { push @{$d->{$k}}, $v }
  2         7  
248             elsif(ref $v eq 'HASH'
249             && $k ne $opts->{contentkey}
250 24         32 && $opts->{forcearray_always}) { push @{$d->{$k}}, $v }
  24         67  
251             elsif($opts->{forcearray_elem}{$k}
252 412         1208 || grep $k =~ $_, @{$opts->{forcearray_regex}}
253 12         18 ) { push @{$d->{$k}}, $v }
  12         38  
254 405         866 else { $d->{$k} = $v }
255 540         1227 $d->{$k};
256             }
257              
258             # Takes the parse tree that XML::LibXML::Parser produced from the supplied
259             # XML and recurse through it 'collapsing' unnecessary levels of indirection
260             # (nested arrays etc) to produce a data structure that is easier to work with.
261              
262             sub _expand_name($)
263 0     0   0 { my $node = shift;
264 0   0     0 my $uri = $node->namespaceURI || '';
265 0 0       0 (length $uri ? "{$uri}" : '') . $node->localName;
266             }
267              
268             sub collapse($$)
269 383     383 0 687 { my ($self, $xml, $opts) = @_;
270 383 50       891 $xml->isa('XML::LibXML::Element') or return;
271              
272 383         546 my (%data, $text);
273 383         567 my $hooks = $self->{XCS_hooks};
274              
275 383 100       661 unless($opts->{noattr})
276             {
277             ATTR:
278 366         734 foreach my $attr ($xml->attributes)
279             {
280 236         1144 my $value;
281 236 50 33     472 if($hooks && (my $hook = $hooks->{$attr->unique_key}))
282 0         0 { $value = $hook->($attr);
283 0 0       0 defined $value or next ATTR;
284             }
285             else
286 236         736 { $value = $attr->value;
287             }
288              
289             $value = $self->normalise_space($value)
290 236 100 66     881 if !ref $value && $opts->{normalisespace}==2;
291              
292             my $name
293             = !$attr->isa('XML::LibXML::Attr') ? $attr->nodeName
294             : $opts->{nsexpand} ? _expand_name($attr)
295 236 50       1003 : $opts->{nsstrip} ? $attr->localName
    50          
    50          
296             : $attr->nodeName;
297              
298 236         454 _add_kv \%data, $name => $value, $opts;
299             }
300             }
301 383         2423 my $nr_attrs = keys %data;
302 383         521 my $nr_elems = 0;
303              
304             CHILD:
305 383         717 foreach my $child ($xml->childNodes)
306             {
307 840 100       4308 if($child->isa('XML::LibXML::Text'))
308 536         1635 { $text .= $child->data;
309 536         1046 next CHILD;
310             }
311              
312 304 50       678 $child->isa('XML::LibXML::Element')
313             or next CHILD;
314              
315 304         399 $nr_elems++;
316              
317 304         383 my $v;
318 304 50 33     669 if($hooks && (my $hook = $hooks->{$child->unique_key}))
319 0         0 { $v = $hook->($child) }
320 304         585 else { $v = $self->collapse($child, $opts) }
321              
322             next CHILD
323 304 0 33     632 if ! defined $v && $opts->{suppressempty};
324              
325             my $name
326             = $opts->{nsexpand} ? _expand_name($child)
327 304 50       1196 : $opts->{nsstrip} ? $child->localName
    50          
328             : $child->nodeName;
329              
330 304         631 _add_kv \%data, $name => $v, $opts;
331             }
332              
333             $text = $self->normalise_space($text)
334 383 100 100     1852 if defined $text && $opts->{normalisespace}==2;
335              
336 383 100 100     4120 return $opts->{forcecontent} ? { $opts->{contentkey} => $text } : $text
    100          
337             if $nr_attrs+$nr_elems==0 && defined $text;
338              
339 247 100 100     669 $data{$opts->{contentkey}} = $text
340             if defined $text && $nr_elems==0;
341              
342             # Roll up 'value' attributes (but only if no nested elements)
343              
344 247 100       506 if(keys %data==1)
345 101         250 { my ($k) = keys %data;
346 101 100       248 return $data{$k} if $opts->{valueattrlist}{$k};
347             }
348              
349             # Turn arrayrefs into hashrefs if key fields present
350              
351 240 100       476 if($opts->{keyattr})
352 233         705 { while(my ($key, $val) = each %data)
353 459 100       1389 { $data{$key} = $self->array_to_hash($key, $val, $opts)
354             if ref $val eq 'ARRAY';
355             }
356             }
357              
358             # disintermediate grouped tags
359              
360 240 100       512 if(my $gr = $opts->{grouptags})
361             {
362             ELEMENT:
363 21         55 while(my ($key, $val) = each %data)
364 43 100       137 { my $sub = $gr->{$key} or next;
365 8 50       19 if(ref $val eq 'ARRAY')
366             { next ELEMENT
367 0 0       0 if grep { keys %$_!=1 || !exists $_->{$sub} } @$val;
  0 0       0  
368 0         0 $data{$key} = { map { %{$_->{$sub}} } @$val };
  0         0  
  0         0  
369             }
370             else
371 8 50 33     30 { ref $val eq 'HASH' && keys %$val==1 or next;
372 8         21 my ($child_key, $child_val) = %$val;
373             $data{$key} = $child_val
374 8 100       40 if $gr->{$key} eq $child_key;
375             }
376             }
377             }
378              
379             # Fold hashes containing a single anonymous array up into just the array
380             return $data{anon}
381             if keys %data == 1
382             && exists $data{anon}
383 240 100 100     640 && ref $data{anon} eq 'ARRAY';
      66        
384              
385             # Suppress empty elements?
386 228 50 66     454 if(! keys %data && exists $opts->{suppressempty}) {
387 0         0 my $sup = $opts->{suppressempty};
388 0 0 0     0 return +(defined $sup && $sup eq '') ? '' : undef;
389             }
390              
391             # Roll up named elements with named nested 'value' attributes
392 228 50       422 if(my $va = $opts->{valueattrlist})
393 228         563 { while(my($key, $val) = each %data)
394 458 50 66     1396 { $va->{$key} && ref $val eq 'HASH' && keys %$val==1 or next;
      66        
395 4         19 $data{$key} = $val->{$va->{$key}};
396             }
397             }
398              
399             $nr_elems+$nr_attrs ? \%data
400             : !defined $text ? {}
401 228 0       603 : $opts->{forcecontent} ? { $opts->{contentkey} => $text }
    50          
    100          
402             : $text;
403             }
404              
405             sub normalise_space($)
406 20     20 0 145 { my $self = shift;
407 20         36 local $_ = shift;
408 20         84 s/^\s+//s;
409 20         80 s/\s+$//s;
410 20         66 s/\s\s+/ /sg;
411 20         49 $_;
412             }
413              
414             # Attempts to 'fold' an array of hashes into an hash of hashes. Returns a
415             # reference to the hash on success or the original array if folding is
416             # not possible. Behaviour is controlled by 'keyattr' option.
417             #
418              
419             sub array_to_hash($$$$)
420 84     84 0 170 { my ($self, $name, $in, $opts) = @_;
421 84         131 my %out;
422              
423 84 50       175 my $ka = $opts->{keyattr} or return $in;
424              
425 84 100       166 if(ref $ka eq 'HASH')
426 28 100       90 { my $newkey = $ka->{$name} or return $in;
427 20         43 my ($key, $flag) = @$newkey;
428              
429 20         39 foreach my $h (@$in)
430 44 100 66     154 { unless(ref $h eq 'HASH' && defined $h->{$key})
431 2 100       16 { warn "<$name> element has no '$key' key attribute\n" if $^W;
432 2         16 return $in;
433             }
434              
435 42         65 my $val = $h->{$key};
436 42 100       77 if(ref $val)
437 2 100       23 { warn "<$name> element has non-scalar '$key' key attribute\n" if $^W;
438 2         16 return $in;
439             }
440              
441             $val = $self->normalise_space($val)
442 40 100       78 if $opts->{normalisespace}==1;
443              
444             warn "<$name> element has non-unique value in '$key' "
445 40 100 100     116 . "key attribute: $val\n" if $^W && defined $out{$val};
446              
447 40         175 $out{$val} = { %$h };
448 40 100       98 $out{$val}{"-$key"} = $out{$val}{$key} if $flag eq '-';
449 40 100       123 delete $out{$val}{$key} if $flag ne '+';
450             }
451             }
452              
453             else # Arrayref
454 56         160 { my $default_keys = "@default_attributes" eq "@$ka";
455              
456             ELEMENT:
457 56         96 foreach my $h (@$in)
458 78 100       239 { ref $h eq 'HASH' or return $in;
459              
460 50         77 foreach my $key (@$ka)
461 81         134 { my $val = $h->{$key};
462 81 100       149 defined $val or next;
463              
464 42 100       80 if(ref $val)
465 2 100 66     25 { warn "<$name> element has non-scalar '$key' key attribute"
466             if $^W && ! $default_keys;
467 2         15 return $in;
468             }
469              
470             $val = $self->normalise_space($val)
471 40 100       84 if $opts->{normalisespace} == 1;
472              
473             warn "<$name> element has non-unique value in '$key' "
474 40 100 100     119 . "key attribute: $val" if $^W && $out{$val};
475              
476 40         167 $out{$val} = { %$h };
477 40         101 delete $out{$val}{$key};
478 40         82 next ELEMENT;
479             }
480 8         90 return $in; # No keyfield matched
481             }
482             }
483              
484             $opts->{collapseagain}
485 34 100       102 or return \%out;
486              
487             # avoid over-complicated structures like
488             # dir => { libexecdir => { content => '$exec_prefix/libexec' },
489             # localstatedir => { content => '$prefix' },
490             # }
491             # into
492             # dir => { libexecdir => '$exec_prefix/libexec',
493             # localstatedir => '$prefix',
494             # }
495              
496 27         44 my $contentkey = $opts->{contentkey};
497              
498             # first go through the values, checking that they are fit to collapse
499 27         71 foreach my $v (values %out)
500 35 50       68 { next if !defined $v;
501 35 100 66     166 next if ref $v eq 'HASH' && keys %$v == 1 && exists $v->{$contentkey};
      100        
502 21 50 33     73 next if ref $v eq 'HASH' && !keys %$v;
503 21         132 return \%out;
504             }
505              
506 6         40 $out{$_} = $out{$_}{$contentkey} for keys %out;
507 6         34 \%out;
508             }
509            
510             1;
511              
512             __END__