File Coverage

blib/lib/XML/Validator/Schema/ModelNode.pm
Criterion Covered Total %
statement 130 148 87.8
branch 21 42 50.0
condition 9 27 33.3
subroutine 27 31 87.1
pod 0 4 0.0
total 187 252 74.2


line stmt bran cond sub pod time code
1             package XML::Validator::Schema::ModelNode;
2 5     5   26 use strict;
  5         10  
  5         154  
3 5     5   22 use warnings;
  5         10  
  5         120  
4 5     5   23 use base 'XML::Validator::Schema::Node';
  5         9  
  5         424  
5 5     5   26 use constant DEBUG => 0;
  5         7  
  5         299  
6              
7 5     5   24 use Carp qw(croak);
  5         9  
  5         222  
8 5     5   24 use XML::Validator::Schema::Util qw(_err _attr);
  5         12  
  5         6162  
9              
10             =head1 NAME
11              
12             XML:Validator::Schema::ModelNode
13              
14             =head1 DESCRIPTION
15              
16             Objects of this class represent the content models encountered while
17             parsing a schema. After a model is completely parsed it is compiled
18             into a regular expression and a human-readbale description and
19             assigned to the element or complex type's 'model' attribute.
20              
21             =cut
22              
23             # parse a model based on a , , or returning the
24             # appropriate subclass
25             sub parse {
26 5     5 0 1335 my ($pkg, $data) = @_;
27 5         12 my $name = $data->{LocalName};
28 5 50 100     44 croak("Unknown model type '$name'")
      66        
      33        
29             unless $name eq 'sequence' or $name eq 'choice' or $name eq 'all'
30             or $name eq 'union';
31              
32             # construct as appropriate
33 5         17 $pkg = "XML::Validator::Schema::" . ucfirst($name) . "ModelNode";
34 5         60 my $self = $pkg->new();
35            
36 5         309 my $min = _attr($data, 'minOccurs');
37 5 50       17 $min = 1 unless defined $min;
38 5 50       29 _err("Invalid value for minOccurs '$min' found in <$name>.")
39             unless $min =~ /^\d+$/;
40 5         14 $self->{min} = $min;
41              
42 5         17 my $max = _attr($data, 'maxOccurs');
43 5 50       16 $max = 1 unless defined $max;
44 5 50 33     25 _err("Invalid value for maxOccurs '$max' found in <$name>.")
45             unless $max =~ /^\d+$/ or $max eq 'unbounded';
46 5         11 $self->{max} = $max;
47              
48 5 100       13 if ($name eq 'all') {
49 1 50 33     7 _err("Found with minOccurs neither 0 nor 1.")
50             unless $self->{min} eq '1' or $self->{min} eq '0';
51 1 50       5 _err("Found with maxOccurs not 1.")
52             unless $self->{max} eq '1';
53             }
54              
55 5         15 return $self;
56             }
57              
58             # compile a tree of elements and model nodes into a single model node
59             # attached to the containing element. This results in a tree
60             # containing only elements and the element having a 'model' object
61             # with working check_model() and check_final_model() methods.
62             sub compile {
63 4     4 0 3404 my $self = shift;
64 4         35 my $root = $self->mother;
65              
66             # the root will get assigned all the ElementNodes composing the model.
67 4         39 $root->clear_daughters;
68              
69             # get two regular expressions, one for verifying the final
70             # composition of the tree and the other for detecting problems
71             # mid-model
72 4         83 my ($final_re, $running_re, $desc) = $self->_compile($root);
73              
74 4         11 $self->{description} = $desc;
75              
76             # hold onto the strings if debugging
77 4         7 $self->{final_re_string} = $final_re if DEBUG;
78 4         4 $self->{running_re_string} = $running_re if DEBUG;
79 4         5 print STDERR "Compile <$root->{name}> content model to:\n\t/$self->{final_re_string}/\n\t/$self->{running_re_string}\n\t$self->{description}\n\n"
80             if DEBUG;
81              
82             # compile the regular expressions
83 4         9 eval {
84 4         144 $self->{final_re} = qr/^$final_re$/;
85 4         82 $self->{running_re} = qr/^$running_re$/;
86             };
87 4 50       16 croak("Problem compiling content model '<$root->{name}>' into regular expression: $@") if $@;
88              
89             # finished
90 4         35 $self->clear_daughters;
91 4         50 $root->{model} = $self;
92             }
93              
94             # recursive worker for compilation of content models. returns three
95             # text fragments - ($final_re, $running_re, $description)
96             sub _compile {
97 5     5   10 my ($self, $root) = @_;
98 5         7 my @final_parts;
99             my @running_parts;
100 0         0 my @desc_parts;
101              
102 5         19 foreach my $d ($self->daughters) {
103 11 100       539 if ($d->isa('XML::Validator::Schema::ElementNode')) {
    50          
104 10         32 my $re_name = quotemeta('<' . $d->{name} . '>');
105 10         31 my $qual = _qual($d->{min}, $d->{max});
106 10 50       28 my $re = length($qual) ? '(?:' . $re_name . ")$qual" : $re_name;
107 10         20 push @final_parts, $re;
108              
109 10 50       33 my $running_qual = _qual($d->{min} eq '0' ? 0 : 1, $d->{max});
110 10 50       23 my $running_re = length($running_qual) ? '(?:' . $re_name . ")$running_qual" : $re_name;
111 10         13 push @running_parts, $running_re;
112              
113 10         24 push @desc_parts, $d->{name} . $qual;
114              
115             # push onto root's daughter list
116 10         34 $root->add_daughter($d);
117              
118             } elsif ($d->isa('XML::Validator::Schema::ModelNode')) {
119             # recurse
120 1         11 my ($final_part, $running_part, $desc)
121             = $d->_compile($root);
122 1         2 push @final_parts, $final_part;
123 1         2 push @running_parts, $running_part;
124 1         3 push @desc_parts, $desc;
125             } else {
126 0         0 croak("What's a " . ref($d) . " doing here?");
127             }
128             }
129            
130             # combine parts into a regex matching the final and running contents
131 5         404 my $final_re = $self->_combine_final_parts(\@final_parts);
132 5         48 my $running_re = $self->_combine_running_parts(\@running_parts);
133 5         19 my $desc = $self->_combine_desc_parts(\@desc_parts);
134              
135 5         25 return ($final_re, $running_re, $desc);
136             }
137              
138             # assign a qualifier based on min/max
139             sub _qual {
140 36     36   51 my ($min, $max) = @_;
141 36 50 33     194 return "" if $min eq '1' and $max eq '1';
142 0 0 0     0 return "+" if $min eq '1' and $max eq 'unbounded';
143 0 0 0     0 return "?" if $min eq '0' and $max eq '1';
144 0 0 0     0 return "*" if $min eq '0' and $max eq 'unbounded';
145 0 0       0 return "{$min,}" if $max eq 'unbounded';
146 0         0 return "{$min,$max}";
147             }
148              
149             # method to check a final content model
150             sub check_final_model {
151 10     10 0 8348 my ($self, $this_name, $names_ref) = @_;
152              
153             # prepare names for regex
154 10         26 my $names = join('', map { '<' . $_ . '>' } @$names_ref);
  17         49  
155              
156 10         14 print STDERR "Checking element string: '$names' against ".
157             "'$self->{final_re_string}'\n" if DEBUG;
158              
159             # do the match and return an error if necessary
160 10 100       80 if ($names !~ /$self->{final_re}/) {
161 4         29 _err("Contents of element '$this_name' do not match content model '$self->{description}'.");
162             }
163             }
164              
165             # method to check content model in mid-parse. will succeed if the set
166             # of names constitute at least a prefix of the required content model.
167             sub check_model {
168 7     7 0 3973 my ($self, $this_name, $names_ref) = @_;
169              
170             # prepare names for regex
171 7         17 my $names = join('', map { '<' . $_ . '>' } @$names_ref);
  9         32  
172              
173 7         10 print STDERR "Checking element string: '$names' against ".
174             "'$self->{running_re_string}'\n" if DEBUG;
175              
176             # do the match and blame $names[-1] for failures
177 7 100       56 if ($names !~ /$self->{running_re}/) {
178 3         17 _err("Inside element '$this_name', element '$names_ref->[-1]' does not match content model '$self->{description}'.");
179             }
180             }
181              
182             package XML::Validator::Schema::SequenceModelNode;
183 5     5   42 use base 'XML::Validator::Schema::ModelNode';
  5         10  
  5         1891  
184              
185             sub _combine_final_parts {
186 3     3   6 my ($self, $parts) = @_;
187              
188             # build final re
189 3         17 my $re = '(?:' . join('', @$parts) . ')' .
190             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
191              
192 3         10 return $re;
193             }
194              
195             sub _combine_running_parts {
196 3     3   8 my ($self, $parts) = @_;
197              
198             # build running re
199 3         6 my $re = join('', map { "(?:$_" } @$parts) .
  7         32  
200             ")?" x @$parts;
201 3         17 $re =~ s!\?$!!;
202 3         11 $re .= XML::Validator::Schema::ModelNode::_qual($self->{min},$self->{max});
203 3         7 return $re;
204             }
205              
206             sub _combine_desc_parts {
207 2     2   5 my ($self, $parts) = @_;
208              
209             # build description
210 2         12 my $desc = '(' . join(',', @$parts) . ')'
211             . XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
212              
213 2         6 return $desc;
214             }
215              
216             package XML::Validator::Schema::ChoiceModelNode;
217 5     5   36 use base 'XML::Validator::Schema::ModelNode';
  5         13  
  5         1437  
218              
219             sub _combine_final_parts {
220 4     4   8 my ($self, $parts) = @_;
221              
222             # build final re
223 4         8 my $re = '(?:' . join('|', map { '(?:'. $_ . ')' } @$parts) . ')' .
  8         31  
224             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
225              
226 4         16 return $re;
227             }
228              
229             sub _combine_running_parts {
230 2     2   4 my ($self, $parts) = @_;
231              
232             # build running re
233 2         43 my $re = '(?:' . $self->_combine_final_parts($parts) . ')' .
234             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
235              
236 2         5 return $re;
237             }
238              
239             sub _combine_desc_parts {
240 2     2   4 my ($self, $parts) = @_;
241              
242             # build description
243 2         10 my $desc = '(' . join('|', @$parts) . ')' .
244             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
245              
246 2         11 return $desc;
247             }
248              
249             package XML::Validator::Schema::UnionModelNode;
250 5     5   31 use base 'XML::Validator::Schema::ModelNode';
  5         9  
  5         1608  
251              
252             sub _combine_final_parts {
253 0     0   0 my ($self, $parts) = @_;
254              
255             # build final re
256 0         0 my $re = '(?:' . join('|', map { '(?:'. $_ . ')' } @$parts) . ')' .
  0         0  
257             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
258              
259 0         0 return $re;
260             }
261              
262             sub _combine_running_parts {
263 0     0   0 my ($self, $parts) = @_;
264              
265             # build running re
266 0         0 my $re = '(?:' . $self->_combine_final_parts($parts) . ')' .
267             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
268              
269 0         0 return $re;
270             }
271              
272             sub _combine_desc_parts {
273 0     0   0 my ($self, $parts) = @_;
274              
275             # build description
276 0         0 my $desc = '(' . join('|', @$parts) . ')' .
277             XML::Validator::Schema::ModelNode::_qual($self->{min}, $self->{max});
278              
279 0         0 return $desc;
280             }
281              
282              
283             package XML::Validator::Schema::AllModelNode;
284 5     5   31 use base 'XML::Validator::Schema::SequenceModelNode';
  5         10  
  5         4238  
285              
286             # an all is just a sequence that doesn't care about ordering and only
287             # accepts min/max of 0/1
288              
289             sub _combine_final_parts {
290 1     1   2 my ($self, $parts) = @_;
291 1         8 return $self->SUPER::_combine_final_parts([sort sort_parts @$parts]);
292             }
293              
294             sub _combine_running_parts {
295 1     1   2 my ($self, $parts) = @_;
296 1         5 return $self->SUPER::_combine_running_parts([sort sort_parts @$parts]);
297             }
298              
299             sub _combine_desc_parts {
300 1     1   3 my ($self, $parts) = @_;
301              
302             # build description
303 1         5 my $desc = '(' . join('&', @$parts) . ')';
304              
305 1         3 return $desc;
306             }
307              
308             # running model check not possible for all, right?
309 0     0   0 sub check_model {}
310              
311             sub check_final_model {
312 3     3   3311 my ($self, $this_name, $names_ref) = @_;
313 3         23 $self->SUPER::check_final_model($this_name, [sort @$names_ref]);
314             }
315              
316             sub sort_parts {
317 2     2   14 my( $a_element ) = $a =~ /<(.*?)\\>/;
318 2         8 my( $b_element ) = $b =~ /<(.*?)\\>/;
319 2         27 $a_element cmp $b_element;
320             }
321              
322             1;