File Coverage

blib/lib/XML/Simple/DTDReader.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w -- -*-cperl-*-
2             package XML::Simple::DTDReader;
3 1     1   28970 use strict;
  1         2  
  1         41  
4 1     1   4 use warnings;
  1         3  
  1         39  
5              
6 1     1   1504 use XML::Parser;
  0            
  0            
7             use Carp;
8             use Cwd;
9             use File::Basename;
10             use Data::Dumper;
11              
12             use vars qw($VERSION @ISA @EXPORT);
13              
14             $VERSION = '0.04';
15              
16             require Exporter;
17             @ISA = qw(Exporter);
18             @EXPORT = qw(XMLin);
19              
20             sub new {
21             my ($class,%opts) = @_;
22             my $self = {%opts};
23             my %h =
24             (
25             Doctype => sub {$self->Doctype(@_)},
26             DoctypeFin => sub {$self->DoctypeFin(@_)},
27             Element => sub {$self->Element(@_)},
28             Attlist => sub {$self->Attlist(@_)},
29             Start => sub {$self->Start(@_)},
30             End => sub {$self->End(@_)},
31             Char => sub {$self->Char(@_)},
32             );
33             $self->{Handlers} = \%h;
34             return bless $self, $class;
35             }
36              
37             sub XMLin {
38             my $self;
39             if(@_ and $_[0] and UNIVERSAL::isa($_[0], __PACKAGE__)) {
40             $self = shift;
41             } else {
42             $self = __PACKAGE__->new;
43             }
44              
45             my ($source) = @_;
46              
47             $self->{DTD} = undef;
48             $self->{Data} = {};
49             $self->{Element} = [$self->{Data}];
50             $self->{Expected} = undef;
51             $self->{Parser} = XML::Parser->new(
52             Handlers => $self->{Handlers},
53             ParseParamEnt => 1,
54             );
55             if (not defined $source) {
56             my($base, $path) = File::Basename::fileparse($0, '\.[^\.]+');
57             $source = "$base.xml";
58             my $cwd = getcwd();
59             chdir $path;
60             open(XML, $source) or croak "Can't open $source: $!";
61             $Carp::CarpLevel = 2;
62             eval {
63             $self->{Parser}->parse(*XML);
64             };
65             chdir $cwd;
66             die $@ if $@;
67             close(XML) or croak "Can't close $source: $!";
68             } elsif ($source =~ /<.*>/ or UNIVERSAL::isa($source, "IO::Handle")) {
69             $Carp::CarpLevel = 2;
70             $self->{Parser}->parse($source);
71             } elsif ($source eq "-") {
72             local $/;
73             $Carp::CarpLevel = 2;
74             $self->{Parser}->parse();
75             } else {
76             open(XML, $source) or croak "Can't open $source: $!";
77             my $cwd = getcwd();
78             chdir dirname($source);
79             $Carp::CarpLevel = 2;
80             eval {
81             $self->{Parser}->parse(*XML);
82             };
83             chdir $cwd;
84             die $@ if $@;
85             close(XML) or croak "Can't close $source: $!";
86             }
87              
88             $self->{Data} = unref($self->{Data});
89             return $self->{Data};
90             }
91              
92             sub unref {
93             if (ref $_[0] eq "ARRAY") {
94             $_[0][$_] = unref($_[0][$_]) for (0..(@{$_[0]}-1));
95             return $_[0];
96             } elsif (ref $_[0] eq "HASH") {
97             $_[0]{$_} = unref($_[0]{$_}) for keys %{$_[0]};
98             return $_[0];
99             } elsif (ref $_[0] eq "SCALAR") {
100             return ${$_[0]};
101             } else {
102             return $_[0];
103             }
104             }
105              
106             sub Doctype {
107             my $self = shift;
108             my ($parser,$base) = @_;
109             $self->{Expected} = $base;
110             }
111              
112             sub DoctypeFin {
113             local $Carp::CarpLevel = 3;
114             my $self = shift;
115             my $parser = shift;
116             unless (defined $self->{DTD}{$self->{Expected}}) {
117             croak "Your DTD claimed the root element would be '$self->{Expected}', but failed to define that element type."
118             }
119             }
120              
121             sub Element {
122             local $Carp::CarpLevel = 3;
123             my $self = shift;
124             my $parser = shift;
125             my ($name, $element) = @_;
126             if ($element->ismixed and $element->asString ne "(#PCDATA)") {
127             croak "XML::Simple::DTDReader cannot handle mixed content ('$name' tag)";
128             } elsif ($element->isany) {
129             croak "XML::Simple::DTDReader cannot handle 'ANY' content ('$name' tag)";
130             }
131             $self->{DTD}{$name}{Element} = $element;
132             }
133              
134             sub Attlist {
135             local $Carp::CarpLevel = 3;
136             my $self = shift;
137             my $parser = shift;
138             $self->{DTD}{$_[0]}{Attlist}{$_[1]} = {type => $_[2], default => $_[3]};
139             }
140              
141             sub choice {
142             local $Carp::CarpLevel = 4;
143             my ($element, $tag) = @_;
144             if ($element->isname) {
145             return $element->name eq $tag;
146             } elsif ($element->isseq) {
147             return choice(($element->children)[0],$tag);
148             } elsif ($element->ischoice) {
149             for ($element->children) {
150             return 1 if choice($_,$tag);
151             }
152             return 0;
153             } else {
154             croak "XML::Simple::DTDReader cannot deal with mixed or ANY tags ($element)";
155             }
156             }
157              
158             sub Start {
159             local $Carp::CarpLevel = 3;
160             my $self = shift;
161             croak "XML::Simple::DTDReader can only work on XML with a DTD" unless defined $self->{DTD};
162              
163             my($parser,$tag, %atts) = @_;
164             unless ($parser->current_element) {
165             # Top level element
166             croak "Root element <$tag> and DTD <", $self->{Expected}, "> do not match" if $tag ne $self->{Expected};
167             $self->{Expected} = [[$self->{DTD}{$self->{Expected}}{Element}]];
168             return;
169             }
170              
171             my $expected;
172             # warn "\n\nSTART $tag\n";
173             STACK: {
174             # warn Dumper($self->{Expected});
175             while (@{$self->{Expected}} and @{$self->{Expected}[0]} == 0) {
176             shift @{$self->{Expected}};
177             }
178             croak "Unexpected element <$tag> found (column ".$parser->current_column.", line ".$parser->current_line.")" unless @{$self->{Expected}};
179             $expected = shift @{$self->{Expected}[0]};
180             if ($expected->isname) {
181             if ($expected->name ne $tag) {
182             redo STACK if $expected->quant and
183             ($expected->quant eq "?" or $expected->quant eq "*");
184             croak "Unexpected element <$tag> (column ".$parser->current_column.", line ".$parser->current_line."), expecting <".$expected->name.">";
185             } elsif ($expected->quant and
186             ($expected->quant eq "+" or $expected->quant eq "*")) {
187             $expected->{Quant} = "*";
188             unshift @{$self->{Expected}[0]}, $expected;
189             }
190             } elsif ($expected->ischoice) {
191             for ($expected->children) {
192             next unless choice($_,$tag);
193             if ($expected->quant and
194             ($expected->quant eq "+" or $expected->quant eq "*")) {
195             $expected->{Quant} = "*";
196             unshift @{$self->{Expected}[0]}, $expected;
197             }
198             unshift @{$self->{Expected}[0]}, $_;
199             redo STACK;
200             }
201             redo STACK if $expected->quant and
202             ($expected->quant eq "?" or $expected->quant eq "*");
203             croak "Unexpected element $tag in ".Dumper($expected);
204             } elsif ($expected->isseq) {
205             unshift @{$self->{Expected}}, [$expected->children];
206             redo STACK;
207             } else {
208             croak "XML::Simple::DTDReader cannot deal with mixed or ANY tags ($expected)";
209             }
210             }
211              
212             unless (defined $self->{DTD}{$tag}) {
213             croak "Definition of element <$tag> (column ".$parser->current_column.", line ".$parser->current_line.") missing from DTD";
214             }
215              
216             if ($self->{DTD}{$tag}{Element}->isseq) {
217             unshift @{$self->{Expected}}, [$self->{DTD}{$tag}{Element}->children];
218             } elsif ($self->{DTD}{$tag}{Element}->ischoice) {
219             unshift @{$self->{Expected}[0]}, $self->{DTD}{$tag}{Element};
220             }
221              
222             if ($self->{DTD}{$tag}{Attlist}) {
223             for (keys %{$self->{DTD}{$tag}{Attlist}}) {
224             croak "Element <$tag> (column ".$parser->current_column.", line ".$parser->current_line.") missing required attribute $_"
225             if $self->{DTD}{$tag}{Attlist}{$_}{default} eq "#REQUIRED" and not defined $atts{$_};
226             }
227             }
228             for (keys %atts) {
229             croak "Attribute $_ on <$tag> (column ".$parser->current_column.", line ".$parser->current_line.") missing from DTD"
230             unless defined $self->{DTD}{$tag}{Attlist} and $self->{DTD}{$tag}{Attlist}{$_};
231             }
232              
233             my $me = undef;
234             if ($self->{DTD}{$tag}{Element}->children
235             or keys %{$self->{DTD}{$tag}{Attlist} || {}}) {
236             $me = {%atts};
237             } elsif ($self->{DTD}{$tag}{Element}->isempty){
238             $me = %atts ? {%atts} : \1;
239             } else {
240             my $m = "";
241             $me = \$m;
242             }
243              
244             if ($expected->quant and
245             ($expected->quant eq "*" or $expected->quant eq "+")) {
246             my @ids = grep {$self->{DTD}{$tag}{Attlist}{$_}{type} eq "ID"}
247             keys %{$self->{DTD}{$tag}{Attlist}};
248             if (@ids) {
249             $self->{Element}[0]{$tag}{$atts{$_}} = $me
250             for @ids;
251             } else {
252             push @{$self->{Element}[0]{$tag}}, $me;
253             }
254             } else {
255             $self->{Element}[0]{$tag} = $me;
256             }
257             unshift @{$self->{Element}}, $me;
258             }
259              
260             sub Char {
261             local $Carp::CarpLevel = 3;
262             my $self = shift;
263             my($parser,$string) = @_;
264              
265             if (ref $self->{Element}[0] eq "SCALAR") {
266             eval { ${$self->{Element}[0]} .= $string; };
267             if ($@ =~ /read-only/) {
268             carp "Character data '$string' (column ".$parser->current_column.", line ".$parser->current_line.") in EMPTY element ignored";
269             }
270             } elsif (ref $self->{Element}[0] eq "HASH" and not $self->{DTD}{$parser->current_element}{Element}->children) {
271             $self->{Element}[0]{content} .= $string;
272             } else {
273             carp "Character data '$string' (column ".$parser->current_column.", line ".$parser->current_line.") ignored" if $string =~ /\S/;
274             }
275             }
276              
277             sub empty {
278             my ($element) = @_;
279             if ($element->isname) {
280             return $element->quant and ($element->quant eq "*" or $element->quant eq "?");
281             } elsif ($element->isseq) {
282             for ($element->children) {
283             return 0 unless empty($_);
284             }
285             return 1;
286             } elsif ($element->ischoice) {
287             for ($element->children) {
288             return 1 if empty($_);
289             }
290             return 0;
291             } else {
292             croak "XML::Simple::DTDReader cannot deal with mixed or ANY tags ($element)";
293             }
294             }
295              
296             sub End {
297             local $Carp::CarpLevel = 3;
298             my $self = shift;
299             my($parser,$tag) = @_;
300             shift @{$self->{Element}};
301             if ($self->{DTD}{$tag}{Element}->isseq) {
302             my @blocking = grep {not empty($_)} @{$self->{Expected}[0]};
303             if (@blocking) {
304             croak "Unexpected end of element <$tag> found (column ".$parser->current_column.", line ".$parser->current_line."), expecting <", $blocking[0]->name, ">";
305             }
306             shift @{$self->{Expected}};
307             }
308             }
309              
310             __END__