File Coverage

blib/lib/XML/Parser/Style/RDF.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package XML::Parser::Style::RDF;
2             ################################################################################
3 1     1   24516 use 5.006;
  1         4  
  1         42  
4 1     1   7 use strict;
  1         1  
  1         34  
5 1     1   5 use warnings;
  1         6  
  1         52  
6              
7 1     1   354 use XML::Parser;
  0            
  0            
8             use Data::Dumper;
9             our $NS_RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
10             our $NS_XML = 'http://www.w3.org/XML/1998/namespace';
11              
12             # I need an event identifier... oh, look at that, it's a URI
13             our $RDF_SYNTAX_GRAMMAR = 'http://www.w3.org/TR/rdf-syntax-grammar/';
14             our $ROOT_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-root-node';
15             our $ELEMENT_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-element-node';
16             our $END_ELEMENT_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-root-node';
17             our $ATTRIBUTE_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-attribute-node';
18              
19             our $PLAIN_LITERAL_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-literal-node';
20             our $TYPED_LITERAL_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-typed-literal-node';
21             our $URI_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-identifier-node';
22             our $BLANK_NODE_ID_EVENT = $RDF_SYNTAX_GRAMMAR . '#section-blank-nodeid-event';
23              
24             our $VERSION = '0.01';
25              
26             # straight from the spec
27             our @coreSyntaxTerms = qw(RDF ID about parseType resource nodeID datatype);
28             our @syntaxTerms = (@coreSyntaxTerms, qw(Description li));
29             our @oldTerms = qw(aboutEach aboutEachPrefix bagID);
30              
31             # only attributes allowed in an RDF document without a namespace
32             our $bareAttribute = qr/^(ID|about|resource|parseType|type)$/;
33              
34             # check that a URI is within the set of valid URIs for a ...
35             our $nodeElementURI = do {
36             my $rdf = quotemeta($NS_RDF);
37             my $terms = join '|',
38             map { quotemeta } @coreSyntaxTerms, 'li', @oldTerms;
39             qr/^(?!$rdf(?:$terms)$)/;
40             };
41             our $propertyElementURI = do {
42             my $rdf = quotemeta($NS_RDF);
43             my $terms = join '|',
44             map { quotemeta } @coreSyntaxTerms, 'Description', @oldTerms;
45             qr/^(?!$rdf(?:$terms)$)/;
46             };
47             our $propertyAttributeURI = do {
48             my $rdf = quotemeta($NS_RDF);
49             my $terms = join '|',
50             map { quotemeta } @coreSyntaxTerms, 'Description', 'li', @oldTerms;
51             qr/^(?!$rdf(?:$terms)$)/;
52             };
53              
54             sub Init {
55             my $parser = shift;
56             $parser->{state} = new XML::Parser::Style::RDF::State;
57              
58             my $root = bless {
59             document_element => undef,
60             children => [],
61             base_uri => undef,
62             language => '',
63             }, $ROOT_EVENT;
64              
65             my $xmlns = { # break glass in emergency
66             rdf => $NS_RDF,
67             xml => $NS_XML,
68             };
69              
70             $parser->{state}->root($root, $xmlns);
71             }
72              
73             sub Final {
74             }
75              
76              
77             sub Start {
78             my $parser = shift;
79             my $element = shift;
80              
81             my %xmlns;
82              
83             my @attributes;
84             my $language;
85             my $base_uri;
86              
87             my %attr = @_;
88             for my $name (keys %attr) {
89             # xmlns="bar"
90             # xmlns:foo="bar"
91             if($name =~ /^xmlns(?::(.*))?$/) {
92             my $ns = $1 || '';
93             # print "$ns => $attr{$name}\n";
94             $xmlns{$ns} = $attr{$name};
95             delete $attr{$name};
96             next;
97             }
98             # all attributes must have a namespace, except $bareAttribute
99             if($name =~ /^(\w+):(\w+)$/) {
100             my $ns = $1;
101             my $local = $2;
102             my $nsname = $parser->{state}->xmlns($ns, \%attr);
103              
104             if($nsname and $nsname eq $NS_XML) {
105             # process xml:lang or xml:base
106             if($local eq 'lang') {
107             $language = $attr{$name};
108             # print "lang: $attr{$name}\n";
109             delete $attr{$name};
110             } elsif($local eq 'base') {
111             $base_uri = $attr{$name};
112             # print "base: $attr{$name}\n";
113             delete $attr{$name};
114             }
115             }
116              
117             if($ns =~ /^xml/i) {
118             # reserved namespace
119             delete $attr{$name};
120             }
121             if($nsname and exists $attr{$name}) {
122             push @attributes, bless {
123             local_name => $local,
124             namespace_name => $ns,
125             string_value => $attr{$name},
126             URI => $nsname . $local,
127             }, $ATTRIBUTE_EVENT;
128             }
129             } elsif($name !~ /$bareAttribute/) {
130             warn "Attribute '$name' forbidden without a namespace\n";
131             delete $attr{$name};
132             }
133             }
134              
135             # once the attributes have been preprocessed, we can process the element
136             unless($element =~ /^(?:(\w+):)?(\w+)$/) {
137             die "Element '$element' is illegal somehow\n";
138             }
139             my $ns_name = $1 || '';
140             my $local_name = $2;
141             my $e = bless {
142             local_name => $local_name,
143             namespace_name => $ns_name,
144             children => [],
145             base_uri => $base_uri,
146             attributes => \@attributes,
147             URI => $parser->{state}->xmlns($ns_name) . $local_name,
148             li_counter => 1,
149             language => $language,
150             subject => undef,
151             }, $ELEMENT_EVENT;
152              
153             $parser->{state}->start_element($e, \%xmlns);
154              
155             # print "@{[ map { $_->{local_name} || 'root' } @{ $parser->{state} } ]}\n";
156             }
157              
158             sub Char {
159             my $parser = shift;
160             my $string = shift;
161              
162             my $self = $parser->{state};
163             my $event = $self->text($string);
164             my $e = $self->[-1];
165             my $production = $e->{_production};
166             $self->$production($event); # FIXME: god-like knowledge here
167             }
168              
169             sub End {
170             my $parser = shift;
171             $parser->{state}->end_element();
172             }
173              
174             package XML::Parser::Style::RDF::State;
175              
176             sub new {
177             my $class = shift;
178             return bless [@_], ref $class || $class;
179             }
180              
181             sub xmlns {
182             my $self = shift;
183             my $ns = shift;
184             my $local = shift || {};
185             for my $xmlns ($local, reverse map { $_->{_xmlns} || {} } @$self) {
186             return $xmlns->{$ns} if $xmlns->{$ns};
187             }
188             return undef;
189             }
190              
191             sub root {
192             my $self = shift;
193             my $root = shift;
194             my $xmlns = shift;
195             $root->{_xmlns} = $xmlns if $xmlns;
196             push @$self, $root;
197             $self->[-1]{_production} = 'doc'; # production doc
198             }
199              
200             # text() from spec
201             sub text {
202             my $self = shift;
203             my $text = shift;
204             my $e = $self->[-1];
205              
206             # must generate a proper literal event
207             #
208             # there is a plain literal event, with a language
209             # and a typed literal event, with a datatype
210              
211             # since the typed literal is a specified literal, we check for that first
212             my $datatype;
213             for my $attr (@{ $e->{attributes} || [] }) {
214             if($attr->{URI} eq $NS_RDF . 'datatype') {
215             $datatype = $attr->{string_value}; # FIXME: xml:base
216             }
217             }
218             my $escape = $text;
219             for($escape) {
220             s/\x5C/\\\\/g; # escape the escape char first. lesson learned hard way
221             s/\x09/\\t/g;
222             s/\x0A/\\n/g;
223             s/\x0D/\\r/g;
224             s/\x22/\\"/g;
225             s{([\x00-\x08\x0B\x0C\x0E\x1F\x7F-\x{FFFF}])}{
226             sprintf("\\u%.4X", ord($1))
227             }eg;
228             s{([\x{10000}-\x{10FFFF}])}{
229             sprintf("\\U%.8X", ord($1))
230             }eg;
231             }
232             my $event;
233             if($datatype) {
234             # clearly this is a typed literal
235             $event = bless {
236             literal_value => $text,
237             literal_datatype => $datatype,
238             string_value => qq{"$escape"} . ($datatype ? "^^<$datatype>" : ""),
239             }, $TYPED_LITERAL_EVENT;
240             } else {
241             $event = bless {
242             literal_value => $text,
243             literal_language => $e->{language},
244             string_value => qq{"$escape"} .
245             ($e->{language} ? "\@$e->{language}" : ""),
246             }, $PLAIN_LITERAL_EVENT;
247             }
248             return $event;
249             }
250              
251              
252             # from the spec:
253             #
254             # root(document-element == RDF,
255             # children == list(RDF))
256             sub doc {
257             # TODO: nothing
258             my $self = shift;
259             my $e = $self->[-1];
260             return if shift;
261              
262             # check that this is indeed rdf:RDF
263             if($e->{URI} ne $NS_RDF . 'RDF') {
264             # invalid RDF doocument
265             die "Invalid RDF document, no element";
266             }
267             $self->[0]{document_element} = $e; # after-the-fact
268              
269             # call the RDF production as well
270             $self->RDF();
271             }
272              
273             # from the spec:
274             #
275             #start-element(URI == rdf:RDF,
276             # attributes == set())
277             #nodeElementList
278             #end-element()
279             sub RDF {
280             # TODO: nothing
281             my $self = shift;
282             my $e = $self->[-1];
283             return if shift;
284              
285             # URI == rdf:RDF
286              
287             # attributes == set()
288             if(@{ $e->{attributes} }) {
289             warn "The following attributes were improperly used in an \n" .
290             join '', map {
291             $_->{namespace_name} ?
292             "\t$_->{namespace_name}:$_->{local_name}\n" :
293             "\t$_->{local_name}\n"
294             } @{ $e->{attributes} };
295             }
296              
297             $e->{_production} = 'nodeElementList';
298             }
299              
300             # from spec:
301             #
302             # ws* (nodeElement ws* )*
303             sub nodeElementList {
304             # TODO: check ws*
305             my $self = shift;
306             my $e = $self->[-1];
307             return if shift;
308              
309             die "Invalid nodeElement $e->{URI}"
310             unless $e->{URI} =~ /$nodeElementURI/;
311              
312             $self->nodeElement();
313             # print "nodeElement: $element->{local_name}\n";
314             # $element->{_production} = 'nodeElementList';
315             }
316              
317             # from spec:
318             #
319             # start-element(URI == nodeElementURIs
320             # attributes == set((idAttr | nodeIdAttr | aboutAttr )?, propertyAttr*))
321             # propertyEltList
322             # end-element()
323             sub nodeElement {
324             my $self = shift;
325             my $e = $self->[-1];
326             return if shift;
327              
328             my @propertyAttr;
329             for my $a (@{ $e->{attributes} }) {
330             # If there is an attribute a with a.URI == rdf:ID,
331             # then e.subject :=
332             # uri(identifier := resolve(e, concat("#", a.string-value)))
333             if($a->{URI} eq $NS_RDF . 'ID') {
334             $e->{subject} = $self->uri($self->resolve("#$a->{string_value}"));
335             }
336             # If there is an attribute a with a.URI == rdf:nodeID,
337             # then e.subject := bnodeid(identifier:=a.string-value)
338             elsif($a->{URI} eq $NS_RDF . 'nodeID') {
339             $e->{subject} = $self->bnodeid($a->{string_value});
340             }
341             # If there is an attribute a with a.URI == rdf:about
342             # then e.subject := uri(identifier := resolve(e, a.string-value))
343             elsif($a->{URI} eq $NS_RDF . 'about') {
344             $e->{subject} = $self->uri($self->resolve($a->{string_value}));
345             }
346              
347             else {
348             push @propertyAttr, $a;
349             }
350             }
351              
352             # If e.subject is empty,
353             # then e.subject := bnodeid(identifier := generated-blank-node-id())
354             $e->{subject} = $self->bnodeid($self->generate_blank_node_id())
355             unless $e->{subject};
356              
357             if($e->{URI} ne $NS_RDF . 'Description') {
358             # If e.URI != rdf:Description
359             # then the following statement is added to the graph:
360             # e.subject.string-value rdf:type .
361             die $e->{subject} unless defined $e->{subject}{string_value};
362             print "$e->{subject}{string_value} <$NS_RDF"."type> <$e->{URI}> .\n";
363             }
364              
365             for my $a (@propertyAttr) {
366             die $e->{subject} unless defined $e->{subject}{string_value};
367             if($a->{URI} eq $NS_RDF . 'type') {
368             print $e->{subject}{string_value} .
369             " <${NS_RDF}type> <$a->{URI}> .\n";
370             } else {
371             my $o = $self->text($a->{string_value});
372             die $o unless defined $o->{string_value};
373             print $e->{subject}{string_value} .
374             " <$a->{URI}> $o->{string_value} .\n";
375             }
376             }
377             $e->{_production} = 'propertyEltList';
378             }
379              
380             {
381             my $Blank = "BlAnKblank";
382             sub generate_blank_node_id {
383             my $self = shift;
384             my $id = $Blank++;
385             return $id;
386             }
387             }
388              
389             sub resolve {
390             my $self = shift;
391             my $uri = shift;
392             return $uri;
393             }
394              
395             sub uri {
396             my $self = shift;
397             my $id = shift;
398             my $event = bless {
399             identifier => $id,
400             string_value => "<$id>",
401             }, $URI_EVENT;
402             return $event;
403             }
404              
405             sub bnodeid {
406             my $self = shift;
407             my $id = shift();
408             my $event = bless {
409             identifier => $id,
410             string_value => "_:$id",
411             }, $BLANK_NODE_ID_EVENT;
412              
413             return $event;
414             }
415              
416             # from spec:
417             #
418             # ws* (propertyElt ws* ) *
419             sub propertyEltList {
420             my $self = shift;
421             my $e = $self->[-1];
422             return if shift;
423              
424             die "Invalid propertyElt $e->{URI}"
425             unless $e->{URI} =~ /$propertyElementURI/;
426              
427             # what can we divine from here?
428             # if there are no attributes, it could be one of:
429             # resourcePropertyElt | literalPropertyElt | emptyPropertyElt
430             #
431             # And we can't know which until we go further
432              
433              
434             # any of the parseType* properties can be divined from attributes,
435             # so we do that here
436             for my $attr (@{ $e->{attributes} }) {
437             # check for a recognized parseType production
438             if($attr->{URI} eq $NS_RDF . 'parseType') {
439             my $type = $attr->{string_value};
440             $e->{_production} =
441             $type eq 'Literal' ? 'parseTypeLiteralPropertyElt' :
442             $type eq 'Resource' ? 'parseTypeResourcePropertyElt' :
443             $type eq 'Collection' ? 'parseTypeCollectionPropertyElt' :
444             'parseTypeOtherPropertyElt' ;
445             }
446             }
447             $e->{_production} = 'propertyElt' unless $e->{_production};
448             }
449              
450             # from spec, after removing parseType*:
451             #
452             # resourcePropertyElt | literalPropertyElt | emptyPropertyElt
453             sub propertyElt {
454             my $self = shift;
455             my $e = $self->[-1];
456             my $parent = $self->[-2];
457             my $text = shift;
458             if(ref $text) {
459             # print "property text() $text->{literal_value}\n"
460             # if $text->{literal_value} =~ /\S/;
461             push @{ $e->{children} }, $text;
462             return;
463             } elsif($text) {
464             # closing tag
465             # if children == set(), emptyPropertyElt
466             # else, literalPropertyElt
467             if(@{ $e->{children} }) {
468             # text event
469             $self->literalPropertyElt(); # redirect
470             } else {
471             $self->emptyPropertyElt(); # redirect
472             }
473             return;
474             }
475             # this was obviously a resourcePropertyElt!
476             $parent->{_production} = 'resourcePropertyElt'; # closing tag goes here
477             $self->resourcePropertyElt(); # redirect
478             }
479              
480             sub emptyPropertyElt {
481             my $self = shift; # no action required
482             my $e = $self->[-1];
483             my $parent = $self->[-2];
484              
485             my $r;
486             my $id;
487             my @propertyAttr;
488             for my $a (@{ $e->{attributes} }) {
489             if($a->{URI} eq $NS_RDF . 'ID') {
490             $id = $a->{string_value};
491             next; # bypass empty = 0
492             }
493             if($a->{URI} eq $NS_RDF . 'resource') {
494             $r = $self->uri($self->resolve($a->{string_value}));
495             } elsif($a->{URI} eq $NS_RDF . 'nodeID') {
496             $r = $self->bnodeid($a->{string_value});
497             } else {
498             push @propertyAttr, $a;
499             }
500             }
501             if(@propertyAttr || $r) {
502             $r = $self->bnodeid($self->generate_blank_node_id()) unless $r;
503             for my $a (@propertyAttr) {
504             if($a->{URI} eq $NS_RDF . 'type') {
505             print $r->{string_value} .
506             " <${NS_RDF}type> <$a->{string_value}> .\n";
507             } else {
508             my $o = $self->text($a->{string_value});
509             print $r->{string_value} .
510             " <$a->{URI}> $o->{string_value} .\n";
511             }
512             }
513             print $parent->{subject}{string_value} .
514             " <$e->{URI}> $r->{string_value} .\n";
515             } else {
516             die $parent unless defined $parent->{subject}{string_value};
517             my $o = $self->text("");
518             print $parent->{subject}{string_value} .
519             " <$e->{URI}> $o->{string_value} .\n";
520             }
521             }
522              
523             sub literalPropertyElt {
524             my $self = shift; # no action required
525             my $e = $self->[-1];
526             my $parent = $self->[-2];
527             die $parent->{subject} unless defined $parent->{subject}{string_value};
528             my $o = $self->text(
529             join '', map { $_->{literal_value} } @{ $e->{children} });
530             die $o unless defined $o->{string_value};
531             print "$parent->{subject}{string_value} <$e->{URI}> $o->{string_value} .\n";
532             }
533              
534             sub resourcePropertyElt {
535             my $self = shift;
536             my $e = $self->[-1];
537             my $close = shift;
538             return if ref $close;
539              
540             if($close) {
541             # assert a triple?
542             my($n) = grep { $_->isa($ELEMENT_EVENT) } @{ $e->{children} };
543             my $parent = $self->[-2];
544             die $parent->{subject} unless defined $parent->{subject}{string_value};
545             die $n unless defined $n->{subject}{string_value};
546             print $parent->{subject}{string_value} .
547             " <$e->{URI}> $n->{subject}{string_value} .\n";
548             # print "$parent->{URI} $e->{URI} ??? .\n";
549             return;
550             }
551              
552             # this is actually a nodeElement?
553             $self->nodeElement();
554             }
555              
556             sub start_element {
557             my $self = shift;
558             my $e = shift;
559             my $xmlns = shift || {};
560             my $parent = $self->[-1];
561             $e->{language} = $parent->{language}
562             unless defined $e->{language};
563             $e->{base_uri} = $parent->{base_uri}
564             unless defined $e->{base_uri};
565             $e->{_xmlns} = $xmlns;
566             push @$self, $e;
567             push @{ $parent->{children} }, $e;
568             my $production = $parent->{_production};
569             $self->$production();
570             }
571              
572             sub end_element {
573             my $self = shift;
574             my $e = $self->[-1];
575             my $production = $e->{_production};
576             $self->$production(1);
577             pop @$self;
578             }
579              
580             1;
581             __END__