File Coverage

blib/lib/Class/XML.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Class::XML;
2              
3             $VERSION = "0.06";
4              
5 7     7   42 use strict;
  7         19  
  7         237  
6 7     7   38 use warnings;
  7         18  
  7         234  
7 7     7   43 use vars qw/$VERSION/;
  7         13  
  7         346  
8 7     7   38 use Carp;
  7         26  
  7         752  
9 7     7   12925 use XML::XPath;
  0            
  0            
10             use XML::XPath::XMLParser;
11             use base qw/
12             XML::XPath::Node::Element
13             Class::Accessor
14             Class::Data::Inheritable
15             /;
16              
17             use XML::XPath::Node ':node_keys';
18              
19             use overload '""' => \&XML::XPath::XMLParser::as_string;
20              
21             sub DEBUG { 0; };
22             #sub DEBUG { 1; };
23              
24             sub _classdata_hashref {
25             my ($package, $name) = @_;
26             $package->mk_classdata($name);
27             $package->$name({});
28             }
29              
30             __PACKAGE__->_classdata_hashref('__group_types');
31              
32             sub _add_hash_plural {
33             my ($package, $meth) = @_;
34             no strict 'refs';
35             *{"${package}::${meth}s"} =
36             sub {
37             my ($package, %hash) = @_;
38             while (my @pair = each %hash) {
39             $package->$meth(@pair);
40             }
41             }
42             }
43              
44             sub _add_group_type {
45             my ($package, $type, $hash) = @_;
46             $package->__group_types()->{$type} = $hash;
47             $package->_classdata_hashref("__${type}");
48             $package->_add_has($type);
49             }
50              
51             __PACKAGE__->_add_hash_plural('_add_group_type');
52              
53             sub has_attributes {
54             my ($package) = @_;
55             foreach (@_[1..$#_]) {
56             $package->has_attribute($_);
57             }
58             }
59              
60             __PACKAGE__->_add_group_types(
61             'attribute' => {
62             'get' => 'getAttribute',
63             'set' => 'setAttribute',
64             'delete' => 'removeAttribute',
65             },
66             'parent' => {
67             'get' => '_get_parent',
68             'set' => '_croak_ro',
69             'delete' => '_croak_ro',
70             },
71             'child' => {
72             'get' => '_get_child',
73             'set' => '_set_child',
74             'delete' => '_delete_child',
75             },
76             'children' => {
77             'get' => '_get_children',
78             'set' => '_set_children',
79             'delete' => '_set_children',
80             },
81             'relation' => {
82             'get' => '_get_relation',
83             'set' => '_get_relation',
84             'delete' => '_croak_ro',
85             },
86             );
87              
88             sub element_name {
89             my ($self, $name) = @_;
90             if (defined $name) {
91             $self->__element_name($name);
92             } else {
93             return $self->__element_name || $self->_default_element_name;
94             }
95             }
96              
97             __PACKAGE__->mk_classdata('__element_name');
98              
99             sub _default_element_name {
100             my ($self) = @_;
101             my $package = ref $self || $self;
102             $package =~ s/.*:://;
103             return $package;
104             }
105              
106             sub _add_has {
107             my ($package, $has) = @_;
108             no strict 'refs';
109             my $meth = "has_${has}";
110             my $classdata = "__${has}";
111             *{"${package}::${meth}"} =
112             sub {
113             my ($package, $name, $class) = @_;
114             $package->mk_accessors($name);
115             my @attrs = %{$package->$classdata()};
116             $package->$classdata({ @attrs, $name, $class });
117             };
118             }
119              
120             sub _get_parent {
121             my ($self, $key, $class) = @_;
122             my $node = $self->getParentNode;
123             return cast($class, $node);
124             }
125              
126             sub _get_child {
127             my ($self, $key, $class) = @_;
128             my ($child, @rest) = $self->_get_children(@_[1..$#_]);
129             $self->_croak("Multiple ${key} children (".(1 + @rest).") found for"
130             ." has_child relation of ".ref($self)) if @rest;
131             return $child;
132             }
133              
134             sub _set_child {
135             my ($self, $key, $class, $new) = @_;
136             $self->_croak("New $key is not an XPath node")
137             unless (ref $new && $new->isa("XML::XPath::Node"));
138             $self->_croak("Incorrect node name ".$new->getName." (expected $key)")
139             unless ($new->getName eq $key);
140             my $old = $self->_get_child($key, $class);
141             if ($old) {
142             $self->_replace_child_node($old => $new);
143             } else {
144             $self->appendChild($new);
145             }
146             }
147              
148             sub _replace_child_node { # Should be replaceChild in XML::XPath really
149             my ($self, $old, $new) = @_;
150             $self->insertAfter($new, $old) if $new;
151             $self->removeChild($old);
152             #my $pos = $old->get_pos;
153             #$new->set_pos($pos);
154             #${$self}->[node_children]->[$pos] = $new;
155             #$old->del_parent_link;
156             }
157              
158             sub _get_children {
159             my ($self, $key, $class) = @_;
160             return map { cast($class, $_); }
161             grep { $_->isElementNode && ($_->getName eq $key) }
162             $self->getChildNodes;
163             }
164              
165             sub _set_children {
166             my ($self, $key, $class, @new) = @_;
167             my @old = $self->_get_children(@_[1..$#_]);
168             my $diff = @new - @old;
169             my $least = ($diff >= 0 ? $#old : $#new);
170             warn "Diff $diff, least $least, new $#new, old $#old" if DEBUG;
171             for (0..$least) {
172             $self->_replace_child_node($old[$_] => $new[$_]);
173             }
174             $least++;
175             if ($diff > 0) {
176             for ($least .. $#new) {
177             $self->appendChild($new[$_]);
178             }
179             } elsif ($diff < 0) {
180             for ($least .. $#old) {
181             $self->removeChild($old[$_]);
182             }
183             }
184             }
185              
186             sub _delete_child {
187             my ($self, $key) = @_;
188             foreach (grep { $_->isElementNode && ($_->getName eq $key) }
189             $self->getChildNodes) {
190             $self->removeChild($_);
191             }
192             }
193              
194             sub _get_relation {
195             my ($self, $key, $spec, @args) = @_;
196             my ($path, $class) = @$spec;
197             $path = sprintf($path, @args);
198             warn "$path -> $class" if DEBUG;
199             return map { cast($class, $_); } ($self->findnodes($path));
200             }
201              
202             sub new {
203             my ($self, @opts) = @_;
204             my %passthru;
205             @passthru{qw(xml ioref filename parser)} = undef;
206             if (@opts == 2 && exists $passthru{$opts[0]} && ref($opts[1]) ne 'HASH') {
207             warn "Calling parse with @opts" if DEBUG;
208             return $self->parse(@opts);
209             } else {
210             warn "Calling create with @opts" if DEBUG;
211             return $self->create(@opts);
212             }
213             }
214              
215             sub create {
216             my ($self, @opts) = @_;
217             my @name = ($self->element_name);
218             my $args;
219             unless (@opts) {
220             # Empty constructor, keep defaults
221             } elsif (!ref $opts[0]) {
222             @name = (shift @opts);
223             if (!ref $opts[0]) {
224             push(@name, shift @opts)
225             }
226             }
227             $args = shift @opts;
228             warn "Constructing name @name" if DEBUG;
229             my $new = cast( $self, XML::XPath::Node::Element->new( @name ));
230             if (ref $args eq 'HASH' || ref $args eq 'ARRAY') {
231             my @construct = (ref $args eq 'HASH' ? %$args : @$args);
232             while (my ($k, $v) = splice(@construct,0,2)) {
233             if ($new->can($k)) {
234             $new->$k($v);
235             } else {
236             $self->_croak("Constructor argument $k ($v) is not valid for "
237             .ref($new));
238             }
239             last unless @construct;
240             }
241             }
242             return $new;
243             }
244              
245             sub parse {
246             my $self = shift;
247             my $parser = XML::XPath::XMLParser->new(@_);
248             my ($root) = $parser->parse()->findnodes('/child::*');
249             warn "Parsed root name ".$root->getName if DEBUG;
250             #my $new = { _xpath_node => $root };
251             cast( $self, $root );
252             }
253              
254             sub _croak_ro {
255             my ($self, $key) = @_;
256             my $caller = caller;
257             $self->_croak("'$caller' cannot alter the value of '${key}' on ".
258             "objects of class '".ref($self)."'");
259             }
260              
261             sub _croak {
262             my ($self, $msg) = @_;
263             Carp::croak($msg || $self);
264             }
265              
266             sub get {
267             my ($self, @keys) = @_;
268             warn "Get called: @_" if DEBUG;
269             if (@keys == 1) {
270             return $self->_do_action("get", @keys);
271             } else {
272             return map { $self->get($_[0]) } @keys;
273             }
274             }
275              
276             sub set {
277             my ($self, @data) = @_;
278             my $action = ((defined $data[1]) ? 'set' : 'delete');
279             $self->_do_action($action, @data);
280             }
281              
282             sub _do_action {
283             my ($self, $type, $key, @args) = @_;
284             keys %{$self->__group_types}; # Reset hash iterator
285             while (my ($k, $v) = each %{$self->__group_types}) {
286             my $group = "__${k}";
287             warn "Checking for $key in $group (".join(',',keys %{$self->$group()}).")"
288             if DEBUG;
289             next unless exists $self->$group()->{$key};
290             my $meth = $v->{$type};
291             warn "Found $key; calling $meth" if DEBUG;
292             unshift(@args, $self->$group()->{$key}) if defined $self->$group()->{$key};
293             return $self->$meth($key, @args);
294             }
295             }
296              
297             sub search_children {
298             my ($self) = @_;
299             my $xpath = $self->_gen_search_expr('./child::', @_[1..$#_]);
300             my @results = $self->findnodes($xpath);
301             NODE: foreach my $node (@results) {
302             next NODE unless $node->isElementNode;
303             my $name = $node->getName;
304             my $class;
305             GROUP: foreach my $group (qw/child children/) {
306             my $meth = "__${group}";
307             $class = $self->$meth()->{$name};
308             last GROUP if defined $class;
309             }
310             next NODE unless defined $class;
311             cast($class, $node);
312             }
313             return @results;
314             }
315              
316             sub _gen_search_expr {
317             my ($self, $axis, $name, $attrs) = @_;
318             if (ref $name eq 'HASH') {
319             $attrs = $name;
320             undef $name;
321             }
322             $name ||= '*';
323             my $xpath = "${axis}${name}";
324             ATTRS: {
325             if ($attrs) {
326             my $count;
327             eval { $count = keys %{$attrs}; };
328             $self->_croak("Attributes for search_children must be a hashref!") if $@;
329             last ATTRS unless $count;
330             my @test;
331             while (my ($k, $v) = each %{$attrs}) {
332             $v =~ s/"/\"/g;
333             push(@test, qq!\@${k} = "${v}"!);
334             }
335             $xpath .= '['.join(' and ', @test).']';
336             }
337             }
338             return $xpath;
339             }
340              
341             sub cast {
342             my ($to, $obj) = @_;
343             warn "Casting $obj (".(ref $obj).") to ".(ref $to || $to) if DEBUG;
344             return $obj unless ref $obj;
345             return $obj if (eval { $obj->isa(ref $to || $to) });
346             unless (ref $to) {
347             eval "use ${to};";
348             Carp::croak $@ if $@;
349             }
350             if ($obj->isa('XML::XPath::NodeImpl')) {
351             my $dummy = bless(\$obj, 'Class::XML::DummyLayer');
352             return bless(\$dummy, ref $to || $to);
353             }
354             return bless($obj, ref $to || $to);
355             }
356              
357             package Class::XML::DummyLayer;
358              
359             use base qw/XML::XPath::Node::Element/;
360              
361             sub DESTROY { }; # This should stop things getting GC'ed unexpectedly
362              
363             =head1 NAME
364              
365             Class::XML - Simple XML Abstraction
366              
367             =head1 SYNOPSIS
368              
369             package Foo;
370              
371             use base qw/Class::XML/;
372              
373             __PACKAGE__->has_attributes(qw/length colour/);
374             __PACKAGE__->has_child('bar' => Bar);
375              
376             package Bar;
377              
378             use base qw/Class::XML/;
379              
380             __PACKAGE__->has_parent('foo');
381             __PACKAGE__->has_attribute('counter');
382              
383             # Meanwhile, in another piece of code -
384              
385             my $foo = Foo->new( xml => # Or filename or ioref or parser
386             qq!! );
387              
388             $foo->length; # Returns "3m"
389             $foo->colour("purple"); # Sets colour to purple
390              
391             print $foo; # Outputs
392              
393             my $new_bar = new Bar; # Creates empty Bar node
394              
395             $new_bar->counter("formica");
396            
397             $foo->bar($new_bar); # Replaces child
398              
399             $new_bar->foo->colour; # Returns "purple"
400              
401             $foo->colour(undef); # Deletes colour attribute
402              
403             print $foo; # Outputs
404              
405             =head1 DESCRIPTION
406              
407             Class::XML is designed to make it reasonably easy to create, consume or modify
408             XML from Perl while thinking in terms of Perl objects rather than the available
409             XML APIs; it was written out of a mixture of frustration that JAXB (for Java)
410             and XMLSerializer (for .Net) provided programming capabilities that simply
411             weren't easy to do in Perl with the existing modules, and the sheer pleasure
412             that I've had using Class::DBI.
413              
414             The aim is to provide a convenient abstraction layer that allows you to put as
415             much of your logic as you like into methods on a class tree, then throw some
416             XML at that tree and get back a tree of objects to work with. It should also be
417             easy to get started with for anybody familiar with Class::DBI (although I
418             doubt you could simply switch them due to the impedance mismatch between XML
419             and relational data) and be pleasant to use from the Template Toolkit.
420              
421             Finally, all Class::XML objects are also XML::XPath nodes so the full power of
422             XPath is available to you if Class::XML doesn't provide a shortcut to what
423             you're trying to do (but if you find it doesn't on a regular basis, contact me
424             and I'll see if I can fix that ;).
425              
426             =head1 DETAILS
427              
428             =head2 Setup
429              
430             =head3 element_name
431              
432             __PACKAGE__->element_name('foo');
433              
434             Sets/gets the default element name for this class. If you don't set it,
435             Class::XML defaults to the last component of the package name - so a class
436             Foo::Bar will by default create 'Bar' elements.
437              
438             Note that his is *not* necessarily the element name of any given instance - you
439             can override this in the constructor or by calling the XML::XPath::Node::Element
440             setName method. But if you're doing that, presumably you know what you're doing
441             and why ...
442              
443             =head3 has_attribute(s)
444              
445             __PACKAGE__->has_attribute('attr');
446             or
447             __PACKAGE__->has_attributes(qw/attr1 attr2 attr3/);
448              
449             Creates accessor method(s) for the named attribute(s). Both can be called as
450             many times as you want and will add the specified attributes to the list. Note
451             that setting an attribute to the empty string does *not* delete it - to do that
452             you need to call
453              
454             $obj->attr( undef );
455              
456             which will delete the attribute entirely from the object. There's nothing to
457             stop you calling the accessor again later to re-create it though.
458              
459             =head2 Relationships
460              
461             =head3 has_parent
462              
463             __PACKAGE__->has_parent('foo');
464              
465             Creates a *read-only* accessor of the specified name that references an
466             instance's parent node in the XML document. Can be specified more than once if
467             you expect the class to be used as a child of more than one different element.
468              
469             =head3 has_child
470              
471             __PACKAGE__->has_child('name' => 'Class::Name');
472              
473             Creates an accessor of the specified name that affects a single child node of
474             that name; a runtime exception will be thrown if the instance has more than one
475             child of that name.
476              
477             When setting you can pass in any object which isa XML::XPath::Node::Element;
478             Class::XML will re-bless it appropriately before it gives you it back later.
479              
480             =head3 has_children
481              
482             __PACKAGE__->has_children('name' => 'Class::Name');
483              
484             Functions identically to has_child except the generated accessor returns an
485             array, and can take one to set all such child nodes at once.
486              
487             =head3 has_relation
488              
489             __PACKAGE__->has_relation('name' => [ '//xpath' => 'Class::Name' ]);
490              
491             Creates a read-only accessor that returns the nodeset specified by evaluating
492             the given XPath expression with the object as the context node, and returning
493             the results as an array of Class::Name objects.
494              
495             You can also specify an XPath expression with %s, %i etc. in it; the result
496             will be run through an sprintf on the arguments to the accessor before being
497             used - for example
498              
499             __PACKAGE__->has_relation('find_person' =>
500             [ '//person[@name="%s"]' => 'Person' ]);
501             ...
502             my @ret = $obj->find_person("Barry"); # Evaluates //person[@name="Barry"]
503              
504             =head2 Constructors
505              
506             =head3 new
507              
508             my $obj = My::Class->new( stuff ... )
509              
510             Tries to DWIM as much as possible; figures out whether you're asking it to
511             parse something or create an object from scratch and passes it to the
512             appropriate method. This also means that any args to the 'new' methods of
513             either XML::XPath::XMLParser or XML::XPath::Node::Element will both work
514             here in almost all cases.
515              
516             =head3 parse
517              
518             my $root = My::Class->parse( xml | filename | ioref | parser => source );
519              
520             All four possible arguments behave pretty much as you'd expect (with the caveat
521             that 'parser' needs to be an XML::Parser object since that's what XML::XPath
522             uses). Returns an object corresponding to the root node of the XML document.
523              
524             =head3 create
525              
526             my $new = My::Class->create( name?, ns?, { opts }? )
527              
528             Creates a new instance of the appropriate class from scratch; 'name' if given
529             will override the one stored in element_name, 'ns' is the namespace prefix for
530             the element and 'opts' if given should be a hashref containing name => value
531             pairs for the initial attributes and children of the object.
532              
533             =head2 Searching
534              
535             =head3 search_children
536              
537             my @res = $obj->search_children( name?, { attr => value, ... }? )
538              
539             Searches the immediate children of the object for nodes of name 'name' (or
540             any name if not given) with attribute-value pairs matching the supplied hash
541             reference (or all nodes matching the name test if not given). Any child for
542             whose name a has_child or has_children relationship has been declared will be
543             returned as an object of the appropriate class; any other node will be returned
544             as a vanilla XML::XPath::Node::Element object.
545              
546             =head2 Utility
547              
548             =head3 cast
549              
550             Class::XML::cast($new_class, $obj);
551              
552             Loads the class specified by $new_class if necessary and then re-blesses $obj
553             into it. Designed for internal use but may come in handy :)
554              
555             =head1 AUTHOR
556              
557             Matt S Trout
558              
559             =head1 LICENSE
560              
561             This library is free software; you can redistribute it and/or modify
562             it under the same terms as Perl itself.
563              
564             =cut
565              
566             1;