File Coverage

lib/XML/Compile/Schema.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 8 8 100.0


line stmt bran cond sub pod time code
1             # Copyrights 2006-2017 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              
6             package XML::Compile::Schema;
7 1     1   15547 use vars '$VERSION';
  1         2  
  1         53  
8             $VERSION = '1.58';
9              
10 1     1   5 use base 'XML::Compile';
  1         2  
  1         279  
11              
12             use warnings;
13             use strict;
14              
15             use Log::Report 'xml-compile', syntax => 'SHORT';
16             use List::Util qw/first/;
17             use XML::LibXML ();
18             use File::Spec ();
19             use File::Basename qw/basename/;
20             use Digest::MD5 qw/md5_hex/;
21              
22             use XML::Compile::Schema::Specs;
23             use XML::Compile::Schema::Instance;
24             use XML::Compile::Schema::NameSpaces;
25             use XML::Compile::Util qw/SCHEMA2001 SCHEMA2001i unpack_type/;
26              
27             use XML::Compile::Translate ();
28              
29              
30             sub init($)
31             { my ($self, $args) = @_;
32             $self->{namespaces} = XML::Compile::Schema::NameSpaces->new;
33             $self->SUPER::init($args);
34              
35             $self->importDefinitions($args->{top}, %$args)
36             if $args->{top};
37              
38             $self->{hooks} = [];
39             if(my $h1 = $args->{hook})
40             { $self->addHook(ref $h1 eq 'ARRAY' ? @$h1 : $h1);
41             }
42             if(my $h2 = $args->{hooks})
43             { $self->addHook($_) for ref $h2 eq 'ARRAY' ? @$h2 : $h2;
44             }
45            
46             $self->{key_rewrite} = [];
47             if(my $kr = $args->{key_rewrite})
48             { $self->addKeyRewrite(ref $kr eq 'ARRAY' ? @$kr : $kr);
49             }
50              
51             $self->{block_nss} = [];
52             $self->blockNamespace($args->{block_namespace});
53              
54             $self->{typemap} = $args->{typemap} || {};
55             $self->{unused_tags} = $args->{ignore_unused_tags};
56              
57             $self;
58             }
59              
60             #--------------------------------------
61              
62              
63             sub addHook(@)
64             { my $self = shift;
65             push @{$self->{hooks}}, @_>1 ? {@_} : defined $_[0] ? shift : ();
66             $self;
67             }
68              
69              
70             sub addHooks(@)
71             { my $self = shift;
72             $self->addHook($_) for @_;
73             $self;
74             }
75              
76              
77             sub hooks(;$)
78             { my $hooks = shift->{hooks};
79             my $dir = shift or return @$hooks;
80             grep +(!$_->{action} || $_->{action} eq $dir), @$hooks;
81             }
82              
83              
84             sub addTypemaps(@)
85             { my $map = shift->{typemap};
86             while(@_ > 1)
87             { my $k = shift;
88             $map->{$k} = shift;
89             }
90             $map;
91             }
92             *addTypemap = \&addTypemaps;
93              
94              
95             sub addSchemas($@)
96             { my ($self, $node, %opts) = @_;
97             defined $node or return ();
98              
99             my @nsopts;
100             foreach my $o (qw/source filename target_namespace
101             element_form_default attribute_form_default/)
102             { push @nsopts, $o => delete $opts{$o} if exists $opts{$o};
103             }
104              
105             UNIVERSAL::isa($node, __PACKAGE__)
106             and error __x"use useSchema(), not addSchemas() for a {got} object"
107             , got => ref $node;
108              
109             UNIVERSAL::isa($node, 'XML::LibXML::Node')
110             or error __x"addSchema() requires an XML::LibXML::Node";
111              
112             $node = $node->documentElement
113             if $node->isa('XML::LibXML::Document');
114              
115             my $nss = $self->namespaces;
116             my @schemas;
117              
118             $self->walkTree
119             ( $node,
120             sub { my $this = shift;
121             return 1 unless $this->isa('XML::LibXML::Element')
122             && $this->localName eq 'schema';
123              
124             my $schema = XML::Compile::Schema::Instance->new($this, @nsopts)
125             or next;
126              
127             $nss->add($schema);
128             push @schemas, $schema;
129             return 0;
130             }
131             );
132             @schemas;
133             }
134              
135              
136             sub useSchema(@)
137             { my $self = shift;
138             foreach my $schema (@_)
139             { error __x"useSchema() accepts only {pkg} extensions, not {got}"
140             , pkg => __PACKAGE__, got => (ref $schema || $schema);
141             $self->namespaces->use($schema);
142             }
143             $self;
144             }
145              
146              
147             sub addKeyRewrite(@)
148             { my $self = shift;
149             unshift @{$self->{key_rewrite}}, @_;
150             defined wantarray ? $self->_key_rewrite(undef) : ();
151             }
152              
153             sub _key_rewrite($)
154             { my $self = shift;
155             my @more = map { ref $_ eq 'ARRAY' ? @$_ : defined $_ ? $_ : () } @_;
156              
157             my ($pref_all, %pref, @other);
158             foreach my $rule (@more, @{$self->{key_rewrite}})
159             { if($rule eq 'PREFIXED') { $pref_all++ }
160             elsif($rule =~ m/^PREFIXED\((.*)\)/) { $pref{$_}++ for split /\,/, $1 }
161             else { push @other, $rule }
162             }
163              
164             ( ( $pref_all ? 'PREFIXED'
165             : keys %pref ? 'PREFIXED('.join(',', sort keys %pref).')'
166             : ()), @other );
167             }
168              
169              
170             sub blockNamespace(@)
171             { my $self = shift;
172             push @{$self->{block_nss}}, @_;
173             }
174              
175             sub _block_nss(@)
176             { my $self = shift;
177             grep defined, map {ref $_ eq 'ARRAY' ? @$_ : $_}
178             @_, @{$self->{block_nss}};
179             }
180              
181             #--------------------------------------
182              
183              
184             sub compile($$@)
185             { my ($self, $action, $type, %args) = @_;
186             defined $type or return ();
187              
188             if(exists $args{validation})
189             { $args{check_values} = $args{validation};
190             $args{check_occurs} = $args{validation};
191             $args{ignore_facets} = ! $args{validation};
192             }
193             else
194             { exists $args{check_values} or $args{check_values} = 1;
195             exists $args{check_occurs} or $args{check_occurs} = 1;
196             }
197              
198             my $iut = exists $args{ignore_unused_tags}
199             ? $args{ignore_unused_tags} : $self->{unused_tags};
200              
201             $args{ignore_unused_tags}
202             = !defined $iut ? undef : ref $iut eq 'Regexp' ? $iut : qr/^/;
203              
204             exists $args{include_namespaces}
205             or $args{include_namespaces} = 1;
206              
207             if($args{sloppy_integers} ||= 0)
208             { eval "require Math::BigInt";
209             panic "require Math::BigInt or sloppy_integers:\n$@"
210             if $@;
211             }
212              
213             if($args{sloppy_floats} ||= 0)
214             { eval "require Math::BigFloat";
215             panic "require Math::BigFloat by sloppy_floats:\n$@" if $@;
216             }
217              
218             if($args{json_friendly} ||= 0)
219             { eval "require Types::Serialiser";
220             panic "require Types::Serialiser by json_friendly:\n$@" if $@;
221             }
222              
223             $args{prefixes} = $self->_namespaceTable
224             (($args{prefixes} || $args{output_namespaces})
225             , $args{namespace_reset}
226             , !($args{use_default_namespace} || $args{use_default_prefix})
227             # use_default_prefix renamed in 0.90
228             );
229              
230             my $nss = $self->namespaces;
231              
232             my ($h1, $h2) = (delete $args{hook}, delete $args{hooks});
233             my @hooks = $self->hooks($action);
234             push @hooks, ref $h1 eq 'ARRAY' ? @$h1 : $h1 if $h1;
235             push @hooks, ref $h2 eq 'ARRAY' ? @$h2 : $h2 if $h2;
236              
237             my %map = ( %{$self->{typemap}}, %{$args{typemap} || {}} );
238             trace "schema compile $action for $type";
239              
240             my @rewrite = $self->_key_rewrite(delete $args{key_rewrite});
241             my @blocked = $self->_block_nss(delete $args{block_namespace});
242              
243             $args{abstract_types} ||= 'ERROR';
244             $args{mixed_elements} ||= 'ATTRIBUTES';
245             $args{default_values} ||= $action eq 'READER' ? 'EXTEND' : 'IGNORE';
246              
247             # Option rename in 0.88
248             $args{any_element} ||= delete $args{anyElement};
249             $args{any_attribute} ||= delete $args{anyAttribute};
250              
251             if(my $xi = $args{xsi_type})
252             { my $nss = $self->namespaces;
253             foreach (keys %$xi)
254             { $xi->{$_} = $nss->autoexpand_xsi_type($_) if $xi->{$_} eq 'AUTO';
255             }
256             }
257              
258             my $transl = XML::Compile::Translate->new
259             ( $action
260             , nss => $self->namespaces
261             );
262              
263             $transl->compile
264             ( $type, %args
265             , hooks => \@hooks
266             , typemap => \%map
267             , rewrite => \@rewrite
268             , block_namespace => \@blocked
269             );
270             }
271              
272             # also used in ::Cache init()
273             sub _namespaceTable($;$$)
274             { my ($self, $table, $reset_count, $block_default) = @_;
275             $table = { reverse @$table }
276             if ref $table eq 'ARRAY';
277              
278             $table->{$_} = { uri => $_, prefix => $table->{$_} }
279             for grep ref $table->{$_} ne 'HASH', keys %$table;
280              
281             if($reset_count)
282             { $_->{used} = 0 for values %$table;
283             }
284              
285             $table->{''} = {uri => '', prefix => '', used => 0}
286             if $block_default && !grep $_->{prefix} eq '', values %$table;
287              
288             # very strong preference for 'xsi'
289             $table->{&SCHEMA2001i} = {uri => SCHEMA2001i, prefix => 'xsi', used => 0};
290              
291             $table;
292             }
293              
294              
295             sub compileType($$@)
296             { my ($self, $action, $type, %args) = @_;
297              
298             # translator can only create elements, not types.
299             my $elem = delete $args{element} || $type;
300             my ($ens, $elocal) = unpack_type $elem;
301             my ($ns, $local) = unpack_type $type;
302              
303             my $SchemaNS = SCHEMA2001;
304              
305             my $defs = $ns ? <<_DIRTY_TRICK1 : <<_DIRTY_TRICK2;
306            
307             targetNamespace="$ens"
308             xmlns:tns="$ns">
309            
310            
311             _DIRTY_TRICK1
312            
313             targetNamespace="$ens"
314             elementFormDefault="unqualified"
315             >
316            
317            
318             _DIRTY_TRICK2
319              
320             $self->importDefinitions($defs);
321             $self->compile($action, $elem, %args);
322             }
323              
324              
325             sub template($@)
326             { my ($self, $action, $type, %args) = @_;
327              
328             my ($to_perl, $to_xml)
329             = $action eq 'PERL' ? (1, 0)
330             : $action eq 'XML' ? (0, 1)
331             : $action eq 'TREE' ? (0, 0)
332             : error __x"template output is either in XML or PERL layout, not '{action}'"
333             , action => $action;
334              
335             my $show
336             = exists $args{show_comments} ? $args{show_comments}
337             : exists $args{show} ? $args{show} # pre-0.79 option name
338             : 'ALL';
339              
340             $show = 'struct,type,occur,facets' if $show eq 'ALL';
341             $show = '' if $show eq 'NONE';
342             my %show = map {("show_$_" => 1)} split m/\,/, $show;
343             my $nss = $self->namespaces;
344              
345             my $indent = $args{indent} || " ";
346             $args{check_occurs} = 1;
347             $args{mixed_elements} ||= 'ATTRIBUTES';
348             $args{default_values} ||= 'EXTEND';
349             $args{abstract_types} ||= 'ERROR';
350              
351             exists $args{include_namespaces}
352             or $args{include_namespaces} = 1;
353              
354             # it could be used to add extra comment lines
355             error __x"typemaps not implemented for XML template examples"
356             if $to_xml && defined $args{typemap} && keys %{$args{typemap}};
357              
358             my @rewrite = $self->_key_rewrite(delete $args{key_rewrite});
359             my @blocked = $self->_block_nss(delete $args{block_namespace});
360              
361             my $table = $args{prefixes} = $self->_namespaceTable
362             (($args{prefixes} || $args{output_namespaces})
363             , $args{namespace_reset}
364             , !$args{use_default_namespace}
365             );
366              
367             my $used = $to_xml && $show{show_type};
368             $table->{&SCHEMA2001}
369             ||= +{prefix => 'xs', uri => SCHEMA2001, used => $used};
370             $table->{&SCHEMA2001i}
371             ||= +{prefix => 'xsi', uri => SCHEMA2001i, used => $used};
372              
373             my $transl = XML::Compile::Translate->new
374             ( 'TEMPLATE'
375             , nss => $self->namespaces
376             );
377              
378             my $compiled = $transl->compile
379             ( $type
380             , %args
381             , rewrite => \@rewrite
382             , block_namespace => \@blocked # not yet supported
383             , output => $action
384             );
385             $compiled or return;
386              
387             my $ast = $compiled->();
388             #use Data::Dumper; $Data::Dumper::Indent = 1; warn Dumper $ast;
389              
390             if($to_perl)
391             { return $transl->toPerl($ast, %show, indent => $indent
392             , skip_header => $args{skip_header})
393             }
394              
395             if($to_xml)
396             { my $doc = XML::LibXML::Document->new('1.1', 'UTF-8');
397             my $node = $transl->toXML($doc, $ast, %show
398             , indent => $indent, skip_header => $args{skip_header});
399             return $node->toString(1);
400             }
401              
402             # return tree
403             $ast;
404             }
405              
406             #------------------------------------------
407              
408              
409             sub namespaces() { shift->{namespaces} }
410              
411              
412             # The cache will certainly avoid penalties by the average module user,
413             # which does not understand the sharing schema definitions between objects
414             # especially in SOAP implementations.
415             my (%schemaByFilestamp, %schemaByChecksum);
416              
417             sub importDefinitions($@)
418             { my ($self, $frags, %options) = @_;
419             my @data = ref $frags eq 'ARRAY' ? @$frags : $frags;
420              
421             # this is a horrible hack, but by far the simpelest solution to
422             # avoid dataToXML process the same info twice.
423             local $self->{_use_cache} = 1;
424              
425             my @schemas;
426             foreach my $data (@data)
427             { defined $data or next;
428             my ($xml, %details) = $self->dataToXML($data);
429             %details = %{delete $options{details}} if $options{details};
430              
431             if(defined $xml)
432             { my @added = $self->addSchemas($xml, %details, %options);
433             if(my $checksum = $details{checksum})
434             { $self->{_cache_checksum}{$checksum} = \@added;
435             }
436             elsif(my $filestamp = $details{filestamp})
437             { $self->{_cache_file}{$filestamp} = \@added;
438             }
439             push @schemas, @added;
440             }
441             elsif(my $filestamp = $details{filestamp})
442             { my $cached = $self->{_cache_file}{$filestamp};
443             $self->namespaces->add(@$cached);
444             }
445             elsif(my $checksum = $details{checksum})
446             { my $cached = $self->{_cache_checksum}{$checksum};
447             $self->namespaces->add(@$cached);
448             }
449             }
450             @schemas;
451             }
452              
453             sub _parseScalar($)
454             { my ($thing, $data) = @_;
455              
456             ref $thing && $thing->{_use_cache}
457             or return $thing->SUPER::_parseScalar($data);
458              
459             my $self = $thing;
460             my $checksum = md5_hex $$data;
461             if($self->{_cache_checksum}{$checksum})
462             { trace "reusing string data with checksum $checksum";
463             return (undef, checksum => $checksum);
464             }
465              
466             trace "cache parsed scalar with checksum $checksum";
467              
468             ( $self->SUPER::_parseScalar($data)
469             , checksum => $checksum
470             );
471             }
472              
473             sub _parseFile($)
474             { my ($thing, $fn) = @_;
475              
476             ref $thing && $thing->{_use_cache}
477             or return $thing->SUPER::_parseFile($fn);
478             my $self = $thing;
479              
480             my ($mtime, $size) = (stat $fn)[9,7];
481             my $filestamp = File::Spec->rel2abs($fn) . '-'. $mtime . '-' . $size;
482              
483             if($self->{_cache_file}{$filestamp})
484             { trace "reusing schemas from file $filestamp";
485             return (undef, filestamp => $filestamp);
486             }
487              
488             trace "cache parsed file $filestamp";
489              
490             ( $self->SUPER::_parseFile($fn)
491             , filestamp => $filestamp
492             );
493             }
494              
495              
496             sub types()
497             { my $nss = shift->namespaces;
498             sort map {$_->types}
499             map {$nss->schemas($_)}
500             $nss->list;
501             }
502              
503              
504             sub elements()
505             { my $nss = shift->namespaces;
506             sort map {$_->elements}
507             map {$nss->schemas($_)}
508             $nss->list;
509             }
510              
511              
512             sub printIndex(@)
513             { my $self = shift;
514             $self->namespaces->printIndex(@_);
515             }
516              
517              
518             sub doesExtend($$)
519             { my $self = shift;
520             $self->namespaces->doesExtend(@_);
521             }
522              
523              
524             1;