File Coverage

lib/XML/Compile/Cache.pm
Criterion Covered Total %
statement 12 12 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 16 16 100.0


line stmt bran cond sub pod time code
1             # Copyrights 2008-2016 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 3     3   159658 use warnings;
  3         4  
  3         111  
6 3     3   12 use strict;
  3         4  
  3         75  
7              
8             package XML::Compile::Cache;
9 3     3   21 use vars '$VERSION';
  3         3  
  3         150  
10             $VERSION = '1.05';
11              
12 3     3   13 use base 'XML::Compile::Schema';
  3         3  
  3         1580  
13              
14             use Log::Report 'xml-compile-cache', syntax => 'SHORT';
15              
16             use XML::Compile::Util qw/pack_type unpack_type/;
17             use List::Util qw/first/;
18             use Scalar::Util qw/weaken/;
19             use XML::LibXML::Simple qw/XMLin/;
20              
21              
22             sub init($)
23             { my ($self, $args) = @_;
24             $self->addPrefixes($args->{prefixes});
25              
26             $self->SUPER::init($args);
27              
28             $self->{XCC_opts} = delete $args->{opts_rw} || [];
29             $self->{XCC_ropts} = delete $args->{opts_readers} || [];
30             $self->{XCC_wopts} = delete $args->{opts_writers} || [];
31             $self->{XCC_undecl} = delete $args->{allow_undeclared} || 0;
32              
33             $self->{XCC_dropts} = {}; # declared opts
34             $self->{XCC_dwopts} = {};
35             $self->{XCC_uropts} = {}; # undeclared opts
36             $self->{XCC_uwopts} = {};
37              
38             $self->{XCC_readers} = {}; # compiled code refs;
39             $self->{XCC_writers} = {};
40              
41             $self->typemap($args->{typemap});
42             $self->xsiType($args->{xsi_type});
43             $self->anyElement($args->{any_element} || 'ATTEMPT');
44              
45             $self;
46             }
47              
48             #----------------------
49              
50              
51             sub typemap(@)
52             { my $self = shift;
53             my $t = $self->{XCC_typemap} ||= {};
54             my @d = @_ > 1 ? @_ : !defined $_[0] ? ()
55             : ref $_[0] eq 'HASH' ? %{$_[0]} : @{$_[0]};
56             while(@d) { my $k = $self->findName(shift @d); $t->{$k} = shift @d }
57             $t;
58             }
59              
60              
61             sub addXsiType(@)
62             { my $self = shift;
63             my $x = $self->{XCC_xsi_type} ||= {};
64             my @d = @_ > 1 ? @_ : !defined $_[0] ? ()
65             : ref $_[0] eq 'HASH' ? %{$_[0]} : @{$_[0]};
66              
67             while(@d)
68             { my $k = $self->findName(shift @d);
69             my $a = shift @d;
70             $a = $self->namespaces->autoexpand_xsi_type($k) || []
71             if $a eq 'AUTO';
72              
73             push @{$x->{$k}}
74             , ref $a eq 'ARRAY' ? (map $self->findName($_), @$a)
75             : $self->findName($a);
76             }
77              
78             $x;
79             }
80             *xsiType = \&addXsiType;
81              
82              
83             sub allowUndeclared(;$)
84             { my $self = shift;
85             @_ ? ($self->{XCC_undecl} = shift) : $self->{XCC_undecl};
86             }
87              
88              
89             sub anyElement($)
90             { my ($self, $anyelem) = @_;
91              
92             # the "$self" in XCC_ropts would create a ref-cycle, causing a
93             # memory leak.
94             my $s = $self; weaken $s;
95              
96             my $code
97             = $anyelem eq 'ATTEMPT' ? sub {$s->_convertAnyTyped(@_)}
98             : $anyelem eq 'SLOPPY' ? sub {$s->_convertAnySloppy(@_)}
99             : $anyelem;
100            
101             $self->addCompileOptions(READERS => any_element => $code);
102             $code;
103             }
104              
105             #----------------------
106              
107              
108             sub addPrefixes(@)
109             { my $self = shift;
110             my $p = $self->{XCC_namespaces} ||= {};
111             my $first = shift;
112             @_ or defined $first
113             or return $p;
114              
115             my @pairs
116             = @_ ? ($first, @_)
117             : ref $first eq 'ARRAY' ? @$first
118             : ref $first eq 'HASH' ? %$first
119             : error __x"prefixes() expects list of PAIRS, an ARRAY or a HASH";
120              
121             my $a = $self->{XCC_prefixes} ||= {};
122             while(@pairs)
123             { my ($prefix, $ns) = (shift @pairs, shift @pairs);
124             $p->{$ns} ||= { uri => $ns, prefix => $prefix, used => 0 };
125              
126             if(my $def = $a->{$prefix})
127             { if($def->{uri} ne $ns)
128             { error __x"prefix `{prefix}' already refers to {uri}, cannot use it for {ns}"
129             , prefix => $prefix, uri => $def->{uri}, ns => $ns;
130             }
131             }
132             else
133             { $a->{$prefix} = $p->{$ns};
134             trace "register prefix $prefix for '$ns'";
135             }
136             }
137             $p;
138             }
139              
140              
141              
142             sub prefixes(@)
143             { my $self = shift;
144             return $self->addPrefixes(@_) if @_;
145             $self->{XCC_namespaces} || {};
146             }
147              
148              
149             sub prefix($) { $_[0]->{XCC_prefixes}{$_[1]} }
150              
151             # [0.995] should this be public?
152             sub byPrefixTable() { shift->{XCC_prefixes} }
153              
154              
155             sub prefixFor($)
156             { my $def = $_[0]->{XCC_namespaces}{$_[1]} or return ();
157             $def->{used}++;
158             $def->{prefix};
159             }
160              
161              
162             sub addNicePrefix($$)
163             { my ($self, $base, $ns) = @_;
164             if(my $def = $self->prefix($base))
165             { return $base if $def->{uri} eq $ns;
166             }
167             else
168             { $self->addPrefixes($base => $ns);
169             return $base;
170             }
171              
172             $base .= '01' if $base !~ m/[0-9]$/;
173             while(my $def = $self->prefix($base))
174             { return $base if $def->{uri} eq $ns;
175             $base++;
176             }
177             $self->addPrefixes($base => $ns);
178             $base;
179             }
180              
181              
182             sub learnPrefixes($)
183             { my ($self, $node) = @_;
184             my $namespaces = $self->prefixes;
185              
186             PREFIX:
187             foreach my $ns ($node->getNamespaces) # learn preferred ns
188             { my ($prefix, $uri) = ($ns->getLocalName, $ns->getData);
189             next if !defined $prefix || $namespaces->{$uri};
190              
191             if(my $def = $self->prefix($prefix))
192             { next PREFIX if $def->{uri} eq $uri;
193             }
194             else
195             { $self->addPrefixes($prefix => $uri);
196             next PREFIX;
197             }
198              
199             $prefix =~ s/0?$/0/;
200             while(my $def = $self->prefix($prefix))
201             { next PREFIX if $def->{uri} eq $uri;
202             $prefix++;
203             }
204             $self->addPrefixes($prefix => $uri);
205             }
206             }
207              
208             sub addSchemas($@)
209             { my ($self, $xml) = (shift, shift);
210             $self->learnPrefixes($xml);
211             $self->SUPER::addSchemas($xml, @_);
212             }
213              
214              
215             sub prefixed($;$)
216             { my $self = shift;
217             my ($ns, $local) = @_==2 ? @_ : unpack_type(shift);
218             $ns or return $local;
219             my $prefix = $self->prefixFor($ns);
220             defined $prefix
221             or error __x"no prefix known for namespace `{ns}', use addPrefixes()"
222             , ns => $ns;
223              
224             length $prefix ? "$prefix:$local" : $local;
225             }
226              
227             #----------------------
228              
229              
230             sub compileAll(;$$)
231             { my ($self, $need, $usens) = @_;
232             my ($need_r, $need_w) = $self->_need($need || 'RW');
233              
234             if($need_r)
235             { foreach my $type (keys %{$self->{XCC_dropts}})
236             { if(defined $usens)
237             { my ($myns, $local) = unpack_type $type;
238             next if $usens eq $myns;
239             }
240             $self->reader($type);
241             }
242             }
243              
244             if($need_w)
245             { foreach my $type (keys %{$self->{XCC_dwopts}})
246             { if(defined $usens)
247             { my ($myns, $local) = unpack_type $type;
248             next if $usens eq $myns;
249             }
250             $self->writer($type);
251             }
252             }
253             }
254              
255              
256             sub _same_params($$)
257             { my ($f, $s) = @_;
258             @$f==@$s or return 0;
259             for(my $i=0; $i<@$f; $i++)
260             { return 0 if !defined $f->[$i] ? defined $s->[$i]
261             : !defined $s->[$i] ? 1 : $f->[$i] ne $s->[$i];
262             }
263             1;
264             }
265              
266             sub reader($@)
267             { my ($self, $name) = (shift, shift);
268             my %args = @_;
269             my $type = $self->findName($name);
270             my $readers = $self->{XCC_readers};
271              
272             if(exists $self->{XCC_dropts}{$type})
273             { trace __x"ignoring options to pre-declared reader {name}"
274             , name => $name if @_;
275              
276             return $readers->{$type}
277             if $readers->{$type};
278             }
279             elsif($self->allowUndeclared)
280             { if(my $ur = $self->{XCC_uropts}{$type})
281             { # do not use cached version when options differ
282             _same_params $ur, \@_
283             or return $args{is_type}
284             ? $self->compileType(READER => $type, @_)
285             : $self->compile(READER => $type, @_);
286             }
287             else
288             { $self->{XCC_uropts}{$type} = \@_;
289             }
290             }
291             elsif(exists $self->{XCC_dwopts}{$type})
292             { error __x"type {name} is only declared as writer", name => $name }
293             else { error __x"type {name} is not declared", name => $name }
294              
295             $readers->{$type} ||= $args{is_type}
296             ? $self->compileType(READER => $type, @_)
297             : $self->compile(READER => $type, @_);
298             }
299              
300              
301             sub writer($%)
302             { my ($self, $name) = (shift, shift);
303             my %args = @_;
304             my $type = $self->findName($name);
305             my $writers = $self->{XCC_writers};
306              
307             if(exists $self->{XCC_dwopts}{$type})
308             { trace __x"ignoring options to pre-declared writer {name}"
309             , name => $name if @_;
310              
311             return $writers->{$type}
312             if $writers->{$type};
313             }
314             elsif($self->{XCC_undecl})
315             { if(my $ur = $self->{XCC_uwopts}{$type})
316             { # do not use cached version when options differ
317             _same_params $ur, \@_
318             or return $args{is_type}
319             ? $self->compileType(WRITER => $type, @_)
320             : $self->compile(WRITER => $type, @_);
321             }
322             else
323             { $self->{XCC_uwopts}{$type} = \@_;
324             }
325             }
326             elsif(exists $self->{XCC_dropts}{$type})
327             { error __x"type {name} is only declared as reader", name => $name;
328             }
329             else
330             { error __x"type {name} is not declared", name => $name;
331             }
332              
333             $writers->{$type} ||= $args{is_type}
334             ? $self->compileType(WRITER => $type, @_)
335             : $self->compile(WRITER => $type, @_);
336              
337             }
338              
339             sub template($$@)
340             { my ($self, $action, $name) = (shift, shift, shift);
341             $action =~ m/^[A-Z]*$/
342             or error __x"missing or illegal action parameter to template()";
343              
344             my $type = $self->findName($name);
345             my @opts = $self->mergeCompileOptions($action, $type, \@_);
346             $self->SUPER::template($action, $type, @opts);
347             }
348              
349              
350             sub addCompileOptions(@)
351             { my $self = shift;
352             my $need = @_%2 ? shift : 'RW';
353              
354             my $set
355             = $need eq 'RW' ? $self->{XCC_opts}
356             : $need eq 'READERS' ? $self->{XCC_ropts}
357             : $need eq 'WRITERS' ? $self->{XCC_wopts}
358             : error __x"addCompileOptions() requires option set name, not {got}"
359             , got => $need;
360              
361             if(ref $set eq 'HASH')
362             { while(@_) { my $k = shift; $set->{$k} = shift } }
363             else { push @$set, @_ }
364             $set;
365             }
366              
367             # Create a list with options for X::C::Schema::compile(), from a list of ARRAYs
368             # and HASHES with options. The later options overrule the older, but in some
369             # cases, the new values are added. This method knows how some of the options
370             # of ::compile() behave. [last update X::C v0.98]
371              
372             sub mergeCompileOptions($$$)
373             { my ($self, $action, $type, $opts) = @_;
374              
375             my @action_opts
376             = ($action eq 'READER' || $action eq 'PERL')
377             ? ($self->{XCC_ropts}, $self->{XCC_dropts}{$type})
378             : ($self->{XCC_wopts}, $self->{XCC_dwopts}{$type});
379              
380             my %p = %{$self->{XCC_namespaces}};
381             my %t = %{$self->{XCC_typemap}};
382             my %x = %{$self->{XCC_xsi_type}};
383             my %opts = (prefixes => \%p, hooks => [], typemap => \%t, xsi_type => \%x);
384              
385             # flatten list of parameters
386             my @take = map {!defined $_ ? () : ref $_ eq 'ARRAY' ? @$_ : %$_ }
387             $self->{XCC_opts}, @action_opts, $opts;
388              
389             while(@take)
390             { my ($opt, $val) = (shift @take, shift @take);
391             defined $val or next;
392              
393             if($opt eq 'prefixes')
394             { my $t = $self->_namespaceTable($val, 1, 0); # expand
395             @p{keys %$t} = values %$t; # overwrite old def if exists
396             }
397             elsif($opt eq 'hooks' || $opt eq 'hook')
398             { my $hooks = $self->_cleanup_hooks($val);
399             unshift @{$opts{hooks}}, ref $hooks eq 'ARRAY' ? @$hooks : $hooks
400             if $hooks;
401             }
402             elsif($opt eq 'typemap')
403             { $val ||= {};
404             if(ref $val eq 'ARRAY')
405             { while(@$val)
406             { my $k = $self->findName(shift @$val);
407             $t{$k} = shift @$val;
408             }
409             }
410             else
411             { while(my($k, $v) = each %$val)
412             { $t{$self->findName($k)} = $v;
413             }
414             }
415             }
416             elsif($opt eq 'key_rewrite')
417             { unshift @{$opts{key_rewrite}}, ref $val eq 'ARRAY' ? @$val : $val;
418             }
419             elsif($opt eq 'xsi_type')
420             { while(my ($t, $a) = each %$val)
421             { my @a = ref $a eq 'ARRAY' ? map($self->findName($_), @$a)
422             : $self->findName($a);
423             push @{$x{$self->findName($t)}}, @a;
424             }
425             }
426             elsif($opt eq 'ignore_unused_tags')
427             { $opts{$opt} = defined $opts{$opt} ? qr/$opts{$opt}|$val/ : $val;
428             }
429             else
430             { $opts{$opt} = $val;
431             }
432             }
433              
434             %opts;
435             }
436              
437             # rewrite hooks
438             sub _cleanup_hooks($)
439             { my ($self, $hooks) = @_;
440             $hooks or return;
441              
442             # translate prefixed type names into full names
443             foreach my $hook (ref $hooks eq 'ARRAY' ? @$hooks : $hooks)
444             { if(my $types = $hook->{type})
445             { $hook->{type} =
446             [ map {ref $_ eq 'Regexp' ? $_ : $self->findName($_)}
447             ref $types eq 'ARRAY' ? @$types : $types ];
448             }
449             elsif(my $ext = $hook->{extends})
450             { $hook->{extends} = $self->findName($ext);
451             }
452             }
453             $hooks;
454             }
455              
456             my %need = (READER => [1,0], WRITER => [0,1], RW => [1,1]);
457             $need{READERS} = $need{READER};
458             $need{WRITERS} = $need{WRITER};
459              
460             sub _need($)
461             { my $need = $need{$_[1]}
462             or error __x"use READER, WRITER or RW, not {dir}", dir => $_[1];
463             @$need;
464             }
465              
466             # support prefixes on types
467             sub addHook(@)
468             { my $self = shift;
469             my $hook = @_ > 1 ? {@_} : shift;
470             $self->_cleanup_hooks($hook);
471             $self->SUPER::addHook($hook);
472             }
473              
474             sub compile($$@)
475             { my ($self, $action, $elem) = splice @_, 0, 3;
476             defined $elem
477             or error __x"compile() requires action and type parameters";
478              
479             $self->SUPER::compile
480             ( $action => $self->findName($elem)
481             , $self->mergeCompileOptions($action, $elem, \@_)
482             );
483             }
484              
485             sub compileType($$@)
486             { my ($self, $action, $type) = splice @_, 0, 3;
487             defined $type
488             or error __x"compileType() requires action and type parameters";
489              
490             $self->SUPER::compileType
491             ( $action => $self->findName($type)
492             , $self->mergeCompileOptions($action, $type, \@_)
493             );
494             }
495              
496             #----------------------
497              
498              
499             sub declare($$@)
500             { my ($self, $need, $names, @opts) = @_;
501             my $opts = @opts==1 ? shift @opts : \@opts;
502             $opts = [ %$opts ] if ref $opts eq 'HASH';
503              
504             my ($need_r, $need_w) = $self->_need($need);
505              
506             foreach my $name (ref $names eq 'ARRAY' ? @$names : $names)
507             { my $type = $self->findName($name);
508             trace "declare $type $need";
509              
510             if($need_r)
511             { defined $self->{XCC_dropts}{$type}
512             and warning __x"reader {name} declared again", name => $name;
513             $self->{XCC_dropts}{$type} = $opts;
514             }
515              
516             if($need_w)
517             { defined $self->{XCC_dwopts}{$type}
518             and warning __x"writer {name} declared again", name => $name;
519             $self->{XCC_dwopts}{$type} = $opts;
520             }
521             }
522              
523             $self;
524             }
525              
526              
527             sub findName($)
528             { my ($self, $name) = @_;
529             defined $name
530             or panic "findName called without name";
531              
532             return $name
533             if substr($name, 0, 1) eq '{';
534              
535             my ($prefix,$local) = $name =~ m/^([\w-]*)\:(\S*)$/ ? ($1,$2) : ('',$name);
536             my $def = $self->{XCC_prefixes}{$prefix};
537             unless($def)
538             { return $name if $prefix eq ''; # namespace-less
539             trace __x"known prefixes: {prefixes}"
540             , prefixes => [ sort keys %{$self->{XCC_prefixes}} ];
541             error __x"unknown name prefix `{prefix}' for `{name}'"
542             , prefix => $prefix, name => $name;
543             }
544              
545             length $local ? pack_type($def->{uri}, $local) : $def->{uri};
546             }
547              
548              
549             sub printIndex(@)
550             { my $self = shift;
551             my $fh = @_ % 2 ? shift : select;
552             my %args = @_;
553             my $decl = exists $args{show_declared} ? delete $args{show_declared} : 1;
554              
555             return $self->SUPER::printIndex($fh, %args)
556             unless $decl;
557              
558             my $output = '';
559             open my($out), '>', \$output;
560              
561             $self->SUPER::printIndex($out, %args);
562              
563             close $out;
564             my @output = split /(?<=\n)/, $output;
565             my $ns = '';
566             foreach (@output)
567             { $ns = $1 if m/^namespace\:\s+(\S+)/;
568             my $local = m/^\s+(\S+)\s*$/ ? $1 : next;
569             my $type = pack_type $ns, $local;
570              
571             substr($_, 1, 1)
572             = $self->{XCC_readers}{$type} ? 'R'
573             : $self->{XCC_dropts}{$type} ? 'r' : ' ';
574              
575             substr($_, 2, 1)
576             = $self->{XCC_writers}{$type} ? 'W'
577             : $self->{XCC_dwopts}{$type} ? 'w' : ' ';
578             }
579             $fh->print(@output);
580             }
581              
582             #---------------
583             # Convert ANY elements and attributes
584              
585             sub _convertAnyTyped(@)
586             { my ($self, $type, $nodes, $path, $read) = @_;
587              
588             my $key = $read->keyRewrite($type);
589             my $reader = try { $self->reader($type) };
590             if($@)
591             { trace "cannot auto-convert 'any': ".$@->wasFatal->message;
592             return ($key => $nodes);
593             }
594             trace "auto-convert known type for 'any': $type";
595              
596             my @nodes = ref $nodes eq 'ARRAY' ? @$nodes : $nodes;
597             my @convert = map $reader->($_), @nodes;
598             ($key => (@convert==1 ? $convert[0] : \@convert) );
599             }
600              
601             sub _convertAnySloppy(@)
602             { my ($self, $type, $nodes, $path, $read) = @_;
603              
604             my $key = $read->keyRewrite($type);
605             my $reader = try { $self->reader($type) };
606             if($@)
607             { # unknown type or untyped...
608             my @convert = map XMLin($_), @$nodes;
609             return ($key => @convert==1 ? $convert[0] : \@convert);
610             }
611             else
612             { trace "auto-convert known 'any' $type";
613             my @nodes = ref $nodes eq 'ARRAY' ? @$nodes : $nodes;
614             my @convert = map $reader->($_), @nodes;
615              
616             ($key => @convert==1 ? $convert[0] : \@convert);
617             }
618             }
619              
620             1;