File Coverage

blib/lib/XMLNews/Meta.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             package XMLNews::Meta;
2              
3 1     1   646 use strict;
  1         1  
  1         45  
4             # The second row consists of static
5             # variables used during XML import.
6             # These variables prevent the module
7             # from being thread-safe or reentrant.
8 1         107 use vars qw($VERSION $XNNS $RDFNS $DCNS %NS_PREFIX_MAP
9 1     1   5 $SELF $DATA @DATA_STACK $READINGPROPS);
  1         1  
10 1     1   1753 use XML::Parser;
  0            
  0            
11             use Carp;
12             use IO;
13              
14             $VERSION = '0.01';
15              
16             #
17             # Static class variables
18             #
19              
20             # Some well-known namespaces...
21             $XNNS = "http://www.xmlnews.org/namespaces/meta#";
22             $RDFNS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
23             $DCNS = "http://www.purl.org/dc#";
24              
25             # Namespace/prefix map for exporting
26             # some commonly-known namespaces
27             # (all others will have auto-generated
28             # prefixes).
29             %NS_PREFIX_MAP = ($XNNS => "xn",
30             $RDFNS => "rdf",
31             $DCNS => "dc");
32              
33              
34             #
35             # Constructor.
36             #
37             sub new {
38             my $class = shift;
39              
40             # The namespaces will be the top-level
41             # keys, mapped to buckets containing
42             # the names in each namespace
43             return bless {}, $class;
44             }
45              
46              
47             #
48             # Return an array of namespaces used in the metadata.
49             #
50             sub getNamespaces {
51             my $self = shift;
52             my @ns = keys(%{$self});
53             my $ns;
54             foreach $ns (@ns) {
55             if ($ns eq $RDFNS) {
56             return @ns;
57             }
58             }
59             push @ns, $RDFNS;
60             return @ns;
61             }
62              
63              
64             #
65             # Return an array of property names used in a namespace.
66             #
67             sub getProperties {
68             my ($self, $namespace) = (@_);
69             my $ns = $self->{$namespace};
70             return () unless $ns;
71             return keys(%{$ns});
72             }
73              
74              
75             #
76             # Assign a value to a property in a namespace.
77             #
78             sub addValue {
79             my ($self, $namespace, $property, $value) = (@_);
80              
81             # Get the namespace.
82             my $ns = $self->{$namespace};
83             unless ($ns) { # Create a new namespace if necessary.
84             $ns = {};
85             $self->{$namespace} = $ns;
86             }
87              
88             # Get the bucket.
89             my $bucket = $ns->{$property};
90             unless ($bucket) { # Create a new bucket if necessary.
91             $bucket = [];
92             $ns->{$property} = $bucket;
93             }
94              
95             # Add the value to the bucket.
96             push @{$bucket}, $value;
97             }
98              
99              
100             #
101             # Return an array of values for a property in a namespace.
102             #
103             sub getValues {
104             my ($self, $namespace, $property) = (@_);
105              
106             # Get the namespace.
107             my $ns = $self->{$namespace};
108             return () unless $ns;
109              
110             # Get the bucket.
111             my $bucket = $ns->{$property};
112             if ($bucket) {
113             return @{$bucket};
114             } else {
115             return ();
116             }
117             }
118              
119              
120             #
121             # Return a single value for a property in a namespace.
122             # This method will croak if the property has more than one value.
123             #
124             sub getValue {
125             my ($self, $namespace, $property) = (@_);
126              
127             # Get the values for this property
128             my @values = $self->getValues($namespace, $property);
129              
130             # Enforce maximum of one value.
131             if ($#values < 1) {
132             return $values[0];
133             } else {
134             croak "Multiple values for $property in $namespace namespace";
135             }
136             }
137              
138              
139             #
140             # Return true if the specified property has at least one value.
141             #
142             sub hasValue {
143             my ($self, $namespace, $property) = (@_);
144             my @values = $self->getValues($namespace, $property);
145              
146             # This is probably a little inefficient
147             # for now, but it will do.
148             if ($#values > -1) {
149             return 1;
150             } else {
151             return undef;
152             }
153             }
154              
155              
156             #
157             # Remove the first occurrence of a value from a property in a namespace.
158             # Carp (don't croak) if we don't find it.
159             #
160             sub removeValue {
161             my ($self, $namespace, $property, $value);
162              
163             # Try to find the namespace
164             my $ns = $self->{$namespace};
165             unless ($ns) {
166             return;
167             }
168              
169             # Try to find the property
170             my $bucket = $ns->{$property};
171             unless ($bucket) {
172             return;
173             }
174              
175             # Remove the first
176             my ($i, $len);
177             LOOP: for ($i = 0, $len = $#{$bucket}; $i <= $len; $i++) {
178             if ($bucket->[$i] eq $value) {
179             splice(@{$bucket}, $i, 1);
180             last LOOP;
181             }
182             }
183              
184              
185             # Remove an empty bucket.
186             if ($#{keys(%{$bucket})} == -1) {
187             delete $ns->{$property};
188             }
189              
190             # Remove an empty namespace.
191             if ($#{keys(%{$ns})} == -1) {
192             delete $self->{$namespace};
193             }
194             }
195              
196              
197             #
198             # Export properties.
199             #
200             sub exportRDF {
201             my ($self, $output) = (@_);
202             my $counter = 0;
203             my %nsmap = (%NS_PREFIX_MAP); # local copy
204              
205             # If the $output argument is a string,
206             # open a file and invoke this method
207             # recursively.
208             unless (ref($output)) {
209             $output = new IO::File(">$output") || croak "Cannot write to file $output";
210             $self->exportRDF($output);
211             $output->close();
212             return;
213             }
214              
215             # Loop up all of the namespaces in use.
216             my @namespaces = $self->getNamespaces();
217             $output->print("\n\n
218              
219             # Make certain that we have a prefix
220             # for every namespace.
221             my $ns;
222             foreach $ns (@namespaces) {
223             if (!$nsmap{$ns}) {
224             $nsmap{$ns} = "p" . $counter++;
225             }
226             $output->print("\n xmlns:" . $nsmap{$ns} . "=\"$ns\"");
227             }
228             $output->print(">\n\n");
229              
230             foreach $ns (@namespaces) {
231             my @properties = $self->getProperties($ns);
232             my $prop;
233             foreach $prop (@properties) {
234             my $name = $nsmap{$ns} . ':' . $prop;
235             my @values = $self->getValues($ns, $prop);
236             my $value;
237             foreach $value (@values) {
238             $output->print("<$name>$value\n");
239             }
240             }
241             }
242              
243             $output->print("\n\n");
244             }
245              
246              
247             #
248             # Import literal properties from an RDF document.
249             # This method is not thread-safe or reentrant.
250             #
251             sub importRDF {
252             my ($self, $input) = (@_);
253             my $parser = $self->_make_parser();
254              
255             # Initialise static variables used
256             # during the parse. It would be
257             # better to use closures, but they
258             # are causing serious memory leaks.
259             $DATA = '';
260             @DATA_STACK = ();
261             $READINGPROPS = 0;
262            
263             unless (ref($input)) {
264             $input = new IO::File("<$input") || croak "Cannot read file $input";
265             $self->importRDF($input);
266             $input->close();
267             return;
268             }
269             $SELF = $self;
270             $parser->parse($input);
271             $SELF = undef;
272             }
273              
274              
275             #
276             # Internal method: handle the start of an element during import.
277             # (This should be a closure, but closures leak badly.)
278             #
279             sub _start {
280             my ($expat, $name) = (@_);
281             my $self = $SELF;
282             my $ns = $expat->namespace($name);
283              
284             # Start capturing data.
285             push @DATA_STACK, $DATA;
286             $DATA = "";
287              
288             # Oops! There is no namespace!
289             unless (defined($ns)) {
290             $expat->xpcarp ("Element $name has no declared namespace\n");
291             }
292              
293             unless ($READINGPROPS) {
294             if ($ns eq $RDFNS && $name eq 'RDF') {
295             # no op!
296             } elsif ($ns eq $RDFNS && $name eq 'Description') {
297             $READINGPROPS = 1;
298             } else {
299             $READINGPROPS = 1;
300             $self->addValue($RDFNS, "type", $ns . $name);
301             }
302             }
303             }
304              
305              
306             #
307             # Internal method: handle the end of an element during import.
308             # (This should be a closure, but closures leak badly.)
309             #
310             sub _end {
311             my ($expat, $name) = (@_);
312             my $self = $SELF;
313             my $ns = $expat->namespace($name);
314              
315             if (!defined($ns)) {
316             $expat->xpcarp("Element $name has no declared namespace\n");
317             } elsif (($ns eq $XNNS && $name eq "Resource") ||
318             ($ns eq $RDFNS && $name eq "Description")){
319             $READINGPROPS = 0;
320             } elsif ($READINGPROPS) {
321             $self->addValue($ns, $name, $DATA);
322             }
323              
324             # Finish capturing data.
325             $DATA = pop @DATA_STACK;
326             }
327              
328              
329             #
330             # Internal method: handle character data during import.
331             # (This should be a closure, but closures leak badly.)
332             #
333             sub _char {
334             my ($expat, $data) = (@_);
335             my $self = $SELF;
336             $DATA .= $data;
337             }
338              
339              
340             #
341             # Create an XML parser.
342             #
343             sub _make_parser {
344             my $self = shift;
345              
346             # Create the actual parser.
347             return new XML::Parser(Handlers => {Start => \&_start,
348             End => \&_end,
349             Char => \&_char},
350             Namespaces => 1);
351             }
352              
353              
354             # Autoload methods go after =cut, and are processed by the autosplit program.
355              
356             1;
357             __END__