File Coverage

blib/lib/XML/TinyXML/Selector/XPath.pm
Criterion Covered Total %
statement 181 198 91.4
branch 82 112 73.2
condition 18 21 85.7
subroutine 17 17 100.0
pod 2 5 40.0
total 300 353 84.9


line stmt bran cond sub pod time code
1             # ex: set tabstop=4:
2              
3             =head1 NAME
4              
5             XML::TinyXML::Selector::XPath - XPath-compliant selector for XML::TinyXML
6              
7             =head1 SYNOPSIS
8              
9             =over 4
10              
11             use XML::TinyXML;
12              
13             # first obtain an xml context:
14             $xml = XML::TinyXML->new("rootnode", param => "somevalue", attrs => { attr1 => v1, attr2 => v2 });
15              
16             $selector = XML::TinyXML::Selector->new($xml, "XPath");
17              
18             #####
19             Assuming the following xml data :
20            
21            
22             world
23            
24            
25            
26            
27            
28            
29            
30            
31            
32             SECOND
33            
34            
35             #####
36              
37             @res = $selector->select('//parent');
38             @res = $selector->select('//child*');
39             @res = $selector->select('/parent[2]/blah/..');
40             @res = $selector->select('//blah/..');
41             @res = $selector->select('//parent[1]/..');
42             @res = $selector->select('//parent[1]/.');
43             @res = $selector->select('//blah/.');
44              
45             # or using the unabbreviated syntax:
46              
47             @res = $selector->select('/descendant-or-self::node()/child::parent');
48             @res = $selector->select('/descendant-or-self::node()/child::child*');
49             @res = $selector->select('/child::parent[2]/child::blah/parent::node()');
50             @res = $selector->select('/descendant-or-self::node()/child::blah/parent::node()');
51             @res = $selector->select('/descendant-or-self::node()/child::parent[1]/parent::node()');
52             @res = $selector->select('/descendant-or-self::node()/child::parent[1]/self::node()');
53             @res = $selector->select('/descendant-or-self::node()/child::blah/self::node()');
54              
55              
56             # refer to XPath documentation for further examples and details on the supported syntax:
57             # ( http://www.w3.org/TR/xpath )
58              
59             =back
60              
61             =head1 DESCRIPTION
62              
63             XPath-compliant selector for XML::TinyXML
64              
65             =head1 INSTANCE VARIABLES
66              
67             =over 4
68              
69             =back
70              
71             =head1 METHODS
72              
73             =over 4
74              
75             =cut
76             package XML::TinyXML::Selector::XPath;
77              
78 4     4   716 use strict;
  4         8  
  4         141  
79 4     4   22 use warnings;
  4         6  
  4         238  
80 4     4   21 use base qw(XML::TinyXML::Selector);
  4         6  
  4         420  
81 4     4   5974 use XML::TinyXML::Selector::XPath::Context;
  4         10  
  4         341  
82 4     4   2481 use XML::TinyXML::Selector::XPath::Functions;
  4         10  
  4         134  
83 4     4   3350 use XML::TinyXML::Selector::XPath::Axes;
  4         12  
  4         18097  
84              
85             our $VERSION = '0.34';
86              
87             #our @ExprTokens = ('(', ')', '[', ']', '.', '..', '@', ',', '::');
88              
89             my @NODE_FUNCTIONS = qw(
90             last
91             position
92             count
93             id
94             local-name
95             namespace-uri
96             name
97             );
98              
99             my @STRING_FUNCTIONS = qw(
100             string
101             concat
102             starts-with
103             contains
104             substring-before
105             substring-after
106             substring
107             string-length
108             normalize-space
109             translate
110             boolean
111             not
112             true
113             false
114             lang
115             );
116              
117             my @NUMBER_FUNCTIONS = qw(
118             number
119             sum
120             floor
121             ceiling
122             round
123             );
124              
125             our @AllFunctions = (@NODE_FUNCTIONS, @STRING_FUNCTIONS, @NUMBER_FUNCTIONS);
126              
127             our @Axes = qw(
128             child
129             descendant
130             parent
131             ancestor
132             following-sibling
133             preceding-sibling
134             following
135             preceding
136             attribute
137             namespace
138             self
139             descendant-or-self
140             ancestor-or-self
141             );
142              
143             =item * init ()
144              
145             =cut
146             sub init {
147 4     4 1 13 my ($self, %args) = @_;
148 4         42 $self->{context} = XML::TinyXML::Selector::XPath::Context->new($self->{_xml});
149 4         24 return $self;
150             }
151              
152             =item * select ($expr, [ $cnode ])
153              
154             =cut
155             sub select {
156 42     42 1 3321 my ($self, $expr) = @_;
157 42         94 my $expanded_expr = $self->_expand_abbreviated($expr);
158 42         104 my $set = $self->_select_unabbreviated($expanded_expr);
159 42 50       138 if ($set) {
160             return wantarray
161 42 0       226 ? @$set
    50          
162             : (scalar(@$set > 1)
163             ? $set
164             : @$set[0])
165             }
166             }
167              
168             sub context {
169 657     657 0 816 my $self = shift;
170 657         2237 return $self->{context};
171             }
172              
173             sub functions {
174 20     20 0 1180 my $self = shift;
175 20 100       100 return wantarray?@AllFunctions:__PACKAGE__."::Functions";
176             }
177              
178             sub resetContext {
179 19     19 0 4885 my $self = shift;
180 19         80 $self->{context} = XML::TinyXML::Selector::XPath::Context->new($self->{_xml});
181             }
182              
183             ###### PRIVATE METHODS ######
184              
185             sub _expand_abbreviated {
186 51     51   228 my ($self, $expr) = @_;
187              
188 51         162 $expr =~ s/\/\//\/descendant-or-self::node()\//g;
189 51         182 my @tokens = split('/', $expr);
190              
191 51         137 foreach my $i (0..$#tokens) {
192 109         169 my $t = $tokens[$i];
193 109 100       232 next unless ($t);
194 87 100       268 if($t !~ /::/) {
195 43 100       129 $t = "child::$tokens[$i]" if ($t !~ /\./);
196 43         73 $t =~ s/\@/attribute::/g;
197 43         104 $t =~ s/\.\./parent::node()/g;
198 43         69 $t =~ s/\./self::node()/g;
199 43         110 $tokens[$i] = $t;
200             }
201             }
202 51         209 join('/', @tokens);
203             }
204              
205             sub _exec_function {
206 8     8   15 my ($self, $fun, @args) = @_;
207 8 50       249 unless(grep(/^$fun$/, @AllFunctions)) {
208 0         0 warn "Unsupported Function: '$fun'";
209 0         0 return undef;
210             }
211 8         26 $fun =~ s/-/_/g;
212 8         43 return XML::TinyXML::Selector::XPath::Functions->$fun($self->{context}, @args);
213             }
214              
215             # Priveate method
216             sub _expand_axis {
217 92     92   122 my ($self, $axis) = @_;
218 92 50       366 if ($axis =~ /(\S+)\s+(\S+)\s+(\S+)/) {
219 0         0 my $a1 = $1;
220 0         0 my $op = $2;
221 0         0 my $a2 = $3;
222              
223 0         0 my $i1 = $self->_expand_axis($a1);
224 0         0 my $i2 = $self->_expand_axis($a2);
225 0         0 return $self->context->operators->{$op}->($i1, $i2);
226             } else {
227 92 50       2073 unless(grep(/^$axis$/, @Axes)) {
228 0         0 warn "Unsupported Axis: '$axis'";
229 0         0 return undef;
230             }
231 92         156 $axis =~ s/-/_/g;
232 92         409 return XML::TinyXML::Selector::XPath::Axes->$axis($self->{context});
233             }
234             }
235              
236             sub _unescape {
237 14     14   24 my ($self, $string) = @_;
238              
239 14 50       43 $string = substr($string, 1, length($string)-2)
240             if ($string =~ /^([\"'])(?:\\\1|.)*?\1$/);
241 14         24 $string =~ s/"/"/g;
242 14         21 $string =~ s/'/'/g;
243 14         14 $string =~ s/&/&/g;
244 14         17 $string =~ s/>/>/g;
245 14         22 $string =~ s/</
246              
247 14         48 return $string;
248             }
249              
250             # Priveate method
251             sub _parse_predicate {
252 12     12   22 my ($self, $predicate) = @_;
253 12         16 my ($attr, $child, $value);
254 0         0 my %res;
255 12 100       130 if ($predicate =~ /^([0-9]+)$/) {
    50          
    50          
    0          
256 6         23 $res{idx} = $1;
257             } elsif (($attr, $value) = $predicate =~ /^\@(\S+)\s*=\s*(.*)\s*$/) {
258 0         0 $res{attr} = $attr;
259 0         0 $res{attr_value} = $self->_unescape($value);
260             } elsif (($child, $value) = $predicate =~ /^(\S+)\s*=\s*(.*)\s*$/) {
261 6         14 $res{child} = $child;
262 6         15 $res{child_value} = $self->_unescape($value);
263             } elsif (($attr) = $predicate =~ /^\@(\S+)$/) {
264 0         0 $res{attr} = $attr;
265             }
266             # TODO - support all predicates
267 12 50       41 return wantarray?%res:\%res;
268             }
269              
270             sub _select_unabbreviated {
271 114     114   175 my ($self, $expr, $recurse) = @_;
272 114         293 my @tokens = split('/', $expr);
273 114         139 my @set;
274 114 100 66     359 if ($expr =~ /^\// and !$recurse) { # absolute path has been requested
275 20         70 $self->context->{items} = [$self->{_xml}->rootNodes()];
276             }
277             # XPath works only in single-root mode
278             # (which is the only allowed mode by the xml spec anyway)
279 114         356 my $state = $self->{_xml}->allowMultipleRootNodes(0);
280             #shift(@tokens)
281             # if (!$tokens[0] and $recurse);
282 114         956 my $token = shift @tokens;
283 114 100 100     862 if ($token and $token =~ /([\w-]+)::([\w\(\)\=]+|\*)(\[.*?\])*$/) {
284 92         183 my $step = $1;
285 92         141 my $nodetest = $2;
286 92         134 my $full_predicate = $3;
287 92         200 @set = $self->_expand_axis($step);
288 92 100       211 if ($nodetest eq '*') {
289 10         23 $self->context->{items} = \@set;
290             } else {
291 82         193 $self->context->{items} = [];
292 82         210 foreach my $node (@set) {
293 502 100       1035 if ($nodetest =~ /\(\)/) {
294 148 50       260 if ($nodetest eq 'node()') {
295 148 50       430 push (@{$self->context->{items}}, $node) if ($node->type ne "ATTRIBUTE");
  148         276  
296             } else {
297 0         0 warn "Unknown NodeTest $nodetest";
298             }
299             } else {
300 354 100       919 push (@{$self->context->{items}}, $node) if ($node->name eq $nodetest);
  90         169  
301             }
302             }
303             }
304 92 100 66     500 if ($full_predicate and $full_predicate =~ s/^\[(.*?)\]$/$1/) {
305 30         64 my @predicates = $full_predicate;
306 30         40 my $op;
307              
308 30         58 my $saved_context = $self->context;
309 30         48 my %all_sets;
310 30   100     163 while ($full_predicate =~ /\(([^()]+)\s+(and|or)\s+([^()]+)\)/ or
311             $full_predicate !~ /^(?:__SET\:\S+__)$/)
312             {
313 31         115 my $tmpctx2 = XML::TinyXML::Selector::XPath::Context->new($self->{_xml});
314 31         143 $tmpctx2->{items} = $saved_context->items;
315 31         218 $self->{context} = $tmpctx2;
316 31 100 66     192 my $inner_predicate = ($1 and $2 and $3)?"$1 $2 $3":$full_predicate;
317 31         177 $inner_predicate =~ s/(^\(|\)$)//g;
318              
319             # TODO - implement full support for complex boolean expression
320 31 100       190 if ($inner_predicate =~ /^(.*?)\s+(and|or)\s+(.*)$/) {
321 4         15 @predicates = ($1, $3);
322 4         7 $op = $2;
323             }
324 31         33 my @itemrefs;
325             # save the actual context to ensure sending the correct context to all predicates
326 31         61 my $saved_context2 = $self->context;
327 31         58 foreach my $predicate_string (@predicates) {
328             # using a temporary context while iterating over all predicates
329 35         116 my $tmpctx = XML::TinyXML::Selector::XPath::Context->new($self->{_xml});
330 35         153 $tmpctx->{items} = $saved_context2->items;
331 35         97 $self->{context} = $tmpctx;
332 35 100       122 if ($predicate_string =~ /^__SET:(\S+)__$/) {
    100          
333 1         2 push(@itemrefs, $all_sets{$1});
334             } elsif ($predicate_string =~ /::/) {
335 22         56 my ($p, $v) = split('=', $predicate_string);
336 22 100       109 $v =~ s/(^['"]|['"]$)//g if ($v); # XXX - unsafe dequoting ... think more to find a better regexp
337 22         30 my %uniq;
338             my @nodepaths;
339 22         54 foreach my $node ($self->_select_unabbreviated($p ,1)) {
340 24 100       75 if ($node->type eq "ATTRIBUTE") {
341 11         45 my $nodepath = $node->node->path;
342 11 100 100     70 next if ($v && $node->value ne $self->_unescape($v));
343 10 50       32 push(@nodepaths, $nodepath) if (!$uniq{$nodepath});
344 10         29 $uniq{$nodepath} = $node->node
345             } else {
346 13         31 my $parent = $node->parent;
347 13 50       28 if ($parent) {
348 13 100 100     39 next if ($v && $node->value ne $v);
349 10 50       30 push(@nodepaths, $parent->path) if (!$uniq{$parent->path});
350 10         27 $uniq{$parent->path} = $parent
351             } else {
352             # TODO - Error Messages
353             }
354             }
355             }
356 22         55 push (@itemrefs, [ map { $uniq{$_} } @nodepaths ]);
  20         79  
357             } else {
358 12         34 my $predicate = $self->_parse_predicate($predicate_string);
359 12 50       53 if ($predicate->{attr}) {
    100          
    50          
360             } elsif ($predicate->{child}) {
361 6 50       29 if ($predicate->{child} =~ s/\(.*?\)//) {
362 6         8 my $func = $predicate->{child};
363 6         14 @set = $self->_exec_function($func); # expand lvalue function
364 6 50       19 if ($predicate->{child_value}) {
365 78         244 my $op_string = join('|',
366             map {
367 6         13 $_ =~ s/([\-\|\+\*\<\>=\!])/\\$1/g;
368 78         143 $_;
369 6         9 } keys(%{$self->context->operators})
370             );
371 6         31 my $value = $predicate->{child_value};
372 6 100       129 if ($value =~ s/\(.*?\)(.*)$//) {
    100          
373 2         3 my $extra = $1;
374 2         6 $value = $self->_exec_function($value); # expand rvalue function
375 2 100       9 if ($extra) {
376 1 50       90 if ($extra =~ /($op_string)(.*)$/) { # check if we must perform an extra operation
377 1         3 $value = $self->context->operators->{$1}->($value, $2);
378             }
379             }
380             } elsif ($value =~ /^(.*?)($op_string)(.*)$/) { # check if we must perform an extra operation
381 2         6 $value = $self->context->operators->{$2}->($1, $3);
382             }
383 6 50       23 if ($func eq 'position') {
384 6         27 my %pos = (@set);
385 6         28 push (@itemrefs, [ $pos{$value} ]);
386             }
387             }
388             } else {
389             }
390             } elsif ($predicate->{idx}) {
391 6         19 push (@itemrefs, [ @{$self->context->items}[$predicate->{idx}-1] ]);
  6         14  
392             }
393             }
394 35         149 $self->{context} = $saved_context2;
395             }
396 31 100       61 if ($op) {
397 4         11 $self->context->{items} = $self->context->operators->{$op}->(@itemrefs);
398             } else {
399 27         59 $self->context->{items} = $itemrefs[0];
400             }
401 31         60 my $id = scalar($self->context->{items});
402 31         58 $all_sets{$id} = $self->context->{items};
403 31 100       77 if ($inner_predicate eq $full_predicate) {
404 29         39 $full_predicate = "";
405 29         71 last;
406             }
407 2 50       6 last unless ($inner_predicate);
408 2         17 $inner_predicate =~ s/([()=])/\\$1/g;
409 2         78 $full_predicate =~ s/\(?$inner_predicate\)?/__SET\:${id}__/;
410             } # while ($full_predicate =~ /\(([^()]+)\)/)
411 30 100       138 if ($full_predicate =~ /__SET:(\S+)__/) {
412 1         4 $self->context->{items} = $all_sets{$1};
413             }
414             } # if ($full_predicate and $full_predicate =~ s/^\[(.*?)\]$/$1/)
415             else {
416 62 50       152 warn "Bad predicate format : '$full_predicate'"
417             if ($full_predicate);
418             }
419             } else {
420 22         23 my @newItems;
421 22         24 foreach my $node (@{$self->context->items}) {
  22         48  
422 33 100       64 if ($token) {
423             # TODO - handle properly, C api has only partial support for predicates
424 12 50       25 if ($token =~ /\[.*?\]/) {
425 0         0 my $child = $node->getChildNodeByName($token);
426 0 0       0 push (@newItems, $child) if ($child);
427             } else {
428 12         33 foreach my $child ($node->children) {
429 11 50       26 push(@newItems, $child)
430             if ($child->name eq $token);
431             }
432             }
433             } else {
434 21         54 push(@newItems, $node);
435             }
436             }
437 22         64 $self->context->{items} = \@newItems;
438             }
439 114 100       249 if (@tokens) {
440 50         279 return $self->_select_unabbreviated(join('/', @tokens), 1); # recursion here
441             }
442 64         210 $self->{_xml}->allowMultipleRootNodes($state);
443 64 100       213 return wantarray?@{$self->context->items}:$self->context->items;
  22         40  
444             }
445              
446             1;
447              
448             =back
449              
450             =head1 SEE ALSO
451              
452             =over 4
453              
454             XML::TinyXML XML::TinyXML::Node XML::TinyXML::Selector
455              
456             =back
457              
458             =head1 AUTHOR
459              
460             xant, Exant@cpan.orgE
461              
462             =head1 COPYRIGHT AND LICENSE
463              
464             Copyright (C) 2009-2010 by xant
465              
466             This library is free software; you can redistribute it and/or modify
467             it under the same terms as Perl itself, either Perl version 5.8.8 or,
468             at your option, any later version of Perl 5 you may have available.
469              
470              
471             =cut
472