File Coverage

blib/lib/XML/DOMBacked.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 XML::DOMBacked;
2              
3 2     2   58765 use strict;
  2         6  
  2         79  
4 2     2   10 use warnings;
  2         4  
  2         61  
5 2     2   1071 use XML::LibXML;
  0            
  0            
6             use LWP::UserAgent;
7              
8             no warnings 'redefine';
9              
10             our $VERSION = '1.00';
11             our $NSMAP = {};
12              
13             use overload
14             'eq' => 'check_equality',
15             '==' => 'check_equality',
16             fallback => 1;
17              
18             sub check_equality {
19             my $lhs = shift;
20             my $rhs = shift;
21             $lhs->dom->isSameNode( $rhs->dom );
22             }
23              
24             sub new {
25             my $class = shift;
26             my $self = {};
27             bless $self => $class;
28             my $init = eval { $self->init( @_ ) };
29             if (!$init) {
30             my $mesg = "could not initialise object";
31             if ( $@ ) { $mesg .= ': ' . $@ }
32             die $mesg;
33             }
34             return $self;
35             }
36              
37             sub from_uri {
38             my $class = shift;
39             my $uri = shift;
40             if (!$uri) {
41             die "need to have URI to load";
42             }
43             my $ua = LWP::UserAgent->new;
44             my $r = $ua->get( $uri );
45             if ( !$r->is_success ) {
46             die "load failed: " . $r->status_line;
47             }
48             my $doc = XML::LibXML->new->parse_string( $r->content );
49             $class->new->dom( $doc->documentElement );
50             }
51              
52              
53             sub init {
54             my $self = shift;
55             $self->dom( XML::LibXML::Element->new( $self->nodename ) );
56             XML::LibXML::Document->new('1.0','UTF-8')->addChild( $self->dom );
57             return "init_call_passed";
58             }
59              
60             sub nodename {
61             my $self = shift;
62             my $class = ref( $self ) || $self;
63             if ( $class =~ /:/ ) {
64             ## we just want to xmlify the last bit
65             return lc( substr($class, rindex( $class, ':') + 1 ) );
66             }
67             return lc( $class );
68             }
69              
70             sub dom {
71             my $self = shift;
72             if ( @_ ) {
73             $self->{ dom } = shift;
74             return $self;
75             }
76             return $self->{ dom };
77             }
78              
79             sub has_many {
80             my $class = shift;
81             my $pairs = { @_ };
82             foreach my $key ( keys %$pairs ) {
83             if ( ref( $pairs->{ $key } ) ) {
84             ## this is a complicated multivalue thing
85             $class->setup_has_many_complex( $key, $pairs->{ $key }->{class} );
86             } else {
87             ## this is a simple multivalue thing.
88             ## What am I talking about they're _all_ complicated multivalue things!
89             $class->setup_has_many_simple( $key, $pairs->{ $key } );
90             }
91             }
92             }
93              
94             sub setup_has_many_complex {
95             my $class = shift;
96             my $key = shift;
97             my $val = shift;
98             my $name = $val->nodename;
99             if ( $name =~ /:/ ) {
100             my ($ns, $attr) = split(/:/, $name);
101             $name = $attr;
102             }
103             # print "Creating thing: $name\n";
104             no strict 'refs';
105             *{$class.'::add_'.$name} = sub {
106             my $self = shift;
107             my $obj = shift;
108             $self->dom->addChild( $obj->dom );
109             };
110             *{$class.'::'.$key} = sub {
111             my $self = shift;
112             map { bless( { dom => $_ }, $val ) } $self->dom->getChildrenByTagName( $val->nodename )
113             };
114             *{$class.'::remove_'.$name} = sub {
115             my $self = shift;
116             my $obj = shift;
117             $self->dom->removeChild( $obj->dom );
118             }
119             }
120              
121             sub setup_has_many_simple {
122             my $class = shift;
123             my $key = shift;
124             my $val = shift;
125             my $name = $val;
126             if ( $val =~ /:/ ) {
127             my ($ns, $attr) = split(/:/, $val);
128             if (!$class->lookup_namespace( $ns )) {
129             die "can't create a property with unknown namespace ($ns)";
130             }
131             $name = $attr;
132             }
133             no strict 'refs';
134             *{$class .'::add_'. $name} = sub {
135             my $self = shift;
136             my $data = shift;
137             my $elem = XML::LibXML::Element->new( $val );
138             $elem->appendText( $data );
139             $self->dom->addChild( $elem );
140             };
141             *{$class .'::'.$key} = sub {
142             my $self = shift;
143             map { $_->findvalue('.') } $self->dom->getChildrenByTagName($val);
144             };
145             *{$class .'::remove_'.$name} = sub {
146             my $self = shift;
147             my $data = shift;
148             my @list = grep { $_->findvalue('.') eq $data } $self->dom->getChildrenByTagName( $val );
149             $self->dom->removeChild( $_ ) for @list;
150             1;
151             };
152             }
153              
154             sub has_a {
155             my $class = shift;
156             foreach my $key ( @_ ) {
157             if ( $key->nodename =~ /:/ ) {
158             ## we're in magic namespace land
159             my ($ns, $rkey) = split(/:/, $key->nodename);
160             my $uri = $class->lookup_namespace( $ns );
161             if (!$uri) {
162             die "can't create a property in an unknown namespace( $ns )";
163             }
164             no strict 'refs';
165             *{ $class . '::' . $rkey } = sub {
166             my $self = shift;
167             if ( @_ ) {
168             $self->set_dom_object( $key, @_ );
169             return $self;
170             }
171             return $self->get_dom_object( $key );
172             };
173             } else {
174             no strict 'refs';
175             # print "Creating in class $key\n";
176             *{ $class . '::' . $key } = sub {
177             my $self = shift;
178             if ( @_ ) {
179             $self->set_dom_object( $key, @_ );
180             return $self;
181             }
182             return $self->get_dom_object( $key );
183             };
184             }
185             }
186             }
187              
188             sub get_dom_object {
189             my $self = shift;
190             my $prop = shift;
191             my $elem = $self->get_property_object( $prop->nodename );
192             my $thing = bless( { dom => $elem }, $prop );
193             return $thing;
194             }
195              
196             sub set_dom_object {
197             my $self = shift;
198             my $prop = shift;
199             my $val = shift;
200              
201             # print "Setting dom object\n";
202             # print $val->as_string;
203              
204             foreach my $ns ( $val->dom->getNamespaces ) {
205             my $prefix = $ns->name;
206             my $uri = $ns->getNamespaceURI;
207             $self->dom->setNamespace( $uri, $prefix, 0 );
208             }
209             $self->dom->addChild( $val->dom );
210             }
211              
212             sub has_attributes {
213             my $class = shift;
214             foreach my $attribute ( @_ ) {
215             if ( $attribute =~ /:/ ) {
216             ## has a namespace attached
217             my ($ns,$rattr) = split(/:/, $attribute);
218             my $uri = $class->lookup_namespace( $ns );
219             # if (! $uri ) {
220             # die "can't create an attribute for an unknown namespace ($ns)";
221             # }
222             no strict 'refs';
223             *{ $class . '::' . $rattr } = sub {
224             my $self = shift;
225             if ( @_ ) {
226             $self->set_dom_attribute( $attribute, @_ );
227             return $self;
228             }
229             return $self->get_dom_attribute( $attribute );
230             };
231             } else {
232             ## straightforward attribute.
233             no strict 'refs';
234             *{ $class . '::' . $attribute } = sub {
235             my $self = shift;
236             if ( @_ ) {
237             $self->set_dom_attribute( $attribute, @_ );
238             return $self;
239             }
240             return $self->get_dom_attribute( $attribute );
241             }
242             }
243             }
244             }
245              
246             sub has_properties {
247             my $class = shift;
248             foreach my $property ( @_ ) {
249             if ( $property =~ /:/ ) {
250             ## this has a namespace attached
251             my ($ns, $realprop) = split(/:/, $property);
252             my $uri = $class->lookup_namespace( $ns );
253             if ( ! $uri ) {
254             die "can't create a property for an unknown namespace";
255             }
256             no strict 'refs';
257             *{ $class . '::' . $realprop } = sub {
258             my $self = shift;
259             if (@_) {
260             $self->set_dom_property( $property, @_ );
261             return $self;
262             }
263             return $self->get_dom_property( $property );
264             };
265             } else {
266             ## this is in the default namespace
267             no strict 'refs';
268             *{ $class . '::' . $property } = sub {
269             my $self = shift;
270             if (@_) {
271             $self->set_dom_property( $property, @_ );
272             return $self;
273             }
274             return $self->get_dom_property( $property );
275             };
276             }
277             }
278             }
279              
280             sub set_dom_attribute {
281             my $self = shift;
282             my $prop = shift;
283             my $val = shift;
284             if ( $prop =~ /:/ ) {
285             my ($ns, $realprop) = split(/:/, $prop);
286             my $uri = $self->lookup_namespace( $ns ) || '';
287             if ( !$self->dom->lookupNamespaceURI( $ns ) ) {
288             $self->dom->setNamespace( $uri, $ns, 0 );
289             } else {
290             $uri = $self->dom->lookupNamespaceURI( $ns );
291             }
292             $self->dom->setAttributeNS( $uri, $realprop, $val );
293             } else {
294             $self->dom->setAttribute( $prop, $val );
295             }
296             }
297              
298             sub get_dom_attribute {
299             my $self = shift;
300             my $prop = shift;
301             if ( $prop =~ /:/ ) {
302             my ($ns, $propname) = split(/:/, $prop);
303             my $uri = $self->lookup_namespace( $ns );
304             if ( $self->dom->hasAttributeNS( $self->lookup_namespace( $ns ), $propname ) ) {
305             my $val = $self->dom->getAttributeNS( $uri, $propname );
306             return $val
307             } else {
308             die "no such property $prop";
309             }
310             } else {
311             return $self->dom->getAttribute( $prop );
312             }
313             }
314              
315             sub get_dom_property {
316             my $self = shift;
317             my $prop = shift;
318             my $elem = $self->get_property_object( $prop );
319             return $elem->findvalue( '.' );
320             }
321              
322             sub set_dom_property {
323             my $self = shift;
324             my $prop = shift;
325             my $data = shift;
326             my $elem = $self->get_property_object( $prop );
327             my $text = XML::LibXML::Text->new( $data );
328             if ( $elem->hasChildNodes() ) {
329             $elem->removeChild( $elem->firstChild );
330             }
331             $elem->addChild( $text );
332             }
333              
334             sub get_property_object {
335             my $self = shift;
336             my $prop = shift;
337              
338             if ( $prop =~ /:/ ) {
339             my ($ns, $rprop) = split(/:/, $prop);
340             my $uri = $self->lookup_namespace( $ns );
341             if ( !$self->dom->lookupNamespacePrefix( $uri ) ) {
342             $self->dom->setNamespace( $uri, $ns, 1 );
343             }
344             my $node = ($self->dom->getChildrenByTagNameNS( $uri, $rprop ))[0];
345             if (!$node) {
346             $node = $self->dom->addNewChild( $uri, $rprop );
347             }
348             return $node;
349              
350             } else {
351             my $node = ($self->dom->getChildrenByTagName($prop))[0];
352             if (! $node ) {
353             $node = XML::LibXML::Element->new( $prop );
354             $self->dom->addChild( $node );
355             }
356             return $node;
357              
358             }
359             }
360              
361             sub lookup_namespace {
362             my $self = shift;
363             my $ns = shift;
364             if (!$ns) {
365             die "need an namespace parameter";
366             }
367             my $class = ref( $self ) || $self;
368             if ( exists $NSMAP->{ $class }->{namespaces}->{ $ns } ) {
369             ## it belongs to this class, which makes life eeee-zeee!
370             return $NSMAP->{$class}->{namespaces}->{ $ns };
371             } else {
372             ## we're in the land of recursion, la la la la lah!
373             ## first we get the IS-A var.
374             no strict 'refs';
375             my @isa = @{ $class . '::ISA' };
376             use strict 'refs';
377             my $uri;
378              
379             ## loop through it until we get an answer
380             foreach my $isa ( @isa ) {
381             my $result = eval { $isa->lookup_namespace( $ns ) };
382             if ( $result ) {
383             $uri = $result;
384             last;
385             }
386             }
387              
388             if ( !exists $NSMAP->{ $class }->{ namespaces }->{ $ns } ) {
389             $NSMAP->{ $class }->{ namespaces }->{ $ns } = $uri;
390             }
391              
392             return $uri;
393             }
394             return 0;
395             }
396              
397             sub uses_namespace {
398             my $class = shift;
399             my $pairs = { @_ };
400             foreach my $key (keys %$pairs) {
401             my $ns = $key;
402             my $uri = $pairs->{$key};
403             $NSMAP->{$class}->{ namespaces }->{ $ns } = $uri;
404             }
405             }
406              
407             sub as_string {
408             my $self = shift;
409             return $self->dom->toString( 1 );
410             }
411              
412             sub as_xml {
413             my $self = shift;
414             my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
415             $doc->setDocumentElement( $self->dom );
416             return $doc->toString( 1 );
417             }
418              
419             1;
420              
421             =head1 NAME
422              
423             XML::DOMBacked - objects backed by a DOM
424              
425             =head1 SYNOPSIS
426              
427             package Person;
428              
429             use base 'XML::DOMBacked';
430              
431             Person->uses_namespace(
432             'foaf' => 'http://xmlns.com/foaf/0.1/',
433             'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
434             );
435             Person->has_properties( 'foaf:name','foaf:title','foaf:nick' );
436             Person->has_attributes( 'rdf:nodeID' );
437             Person->has_a( 'Person::Knows' );
438              
439             sub nodename { "foaf:Person" }
440              
441             package Person::Knows;
442              
443             use base 'XML::DOMBacked';
444              
445             Person::Knows->has_many( people => { class => 'Person' } );
446              
447             package main;
448              
449             my $p = Person->new;
450             $p->nodeID("me");
451             $p->name('A. N. Other');
452             $p->title('Mr');
453             $p->nick('another');
454              
455             my $a = Person->new;
456             $a->name('Yet Another');
457              
458             $p->Knows->add_Person( $a );
459             print $p->as_xml;
460              
461             $p = Person->from_uri( 'file:person.xml' );
462              
463             =head1 DESCRIPTION
464              
465             The C class lets you back an object on an XML DOM. Think of it as Class::DBI
466             for XML files. You can specifiy things you want to be properties (nodes), attributes, and
467             other objects. XML::DOMBacked takes care of the heavy lifting so that you don't have to.
468              
469             =head1 CONSTRUCTORS
470              
471             =over 4
472              
473             =item new()
474              
475             Constructs a new object.
476              
477             =item from_uri()
478              
479             Loads an object from a URI. Expects XML at the other end.
480              
481             =back
482              
483             =head1 METHODS
484              
485             =over 4
486              
487             =item uses_namespace( prefix => uri )
488              
489             Adds an XML namespace to the object.
490              
491             =item has_properties( ARRAY )
492              
493             Adds XML Elements to the object. These become accessors.
494              
495             =item has_attributes( ARRAY )
496              
497             Adds XML Attributes to the object. These become accessors.
498              
499             =item has_a( ARRAY )
500              
501             Adds 1..1 relationships with other classes to the object. The other classes
502             must also inherit from XML::DOMBacked.
503              
504             =item has_many( PLURAL => SINGULAR )
505              
506             Adds add_SINGULAR, remove_SINGLUAR and PLURAL methods to the class.
507              
508             =item has_many( PLURAL => { class => CLASS } )
509              
510             Looks up the NODENAME for the class, then creates add_NODENAME, remove_NODENAME, and PLURAL methods to the class.
511              
512             =back
513              
514             =head1 BUGS
515              
516             Probably loads. This is really funky, crazy code. I'd be surprised if there aren't bugs.
517              
518             =head1 AUTHOR
519              
520             James A. Duncan
521              
522             =head1 COPYRIGHT
523              
524             Copyright 2005 Fotango Ltd. All Rights Reserved.
525              
526             =cut