File Coverage

blib/lib/Data/asXML.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package Data::asXML;
2              
3             =encoding utf-8
4              
5             =head1 NAME
6              
7             Data::asXML - convert data structures to/from XML
8              
9             =head1 SYNOPSIS
10              
11             use Data::asXML;
12             my $dxml = Data::asXML->new();
13             my $dom = $dxml->encode({
14             'some' => 'value',
15             'in' => [ qw(a data structure) ],
16             });
17              
18             my $data = $dxml->decode(q{
19            
20             value
21            
22            
23             a
24             data
25             structure
26            
27            
28            
29             });
30              
31             my (%hash1, %hash2);
32             $hash1{other}=\%hash2;
33             $hash2{other}=\%hash1;
34             print Data::asXML->new->encode([1, \%hash1, \%hash2])->toString;
35            
36            
37             1
38            
39            
40            
41            
42            
43            
44            
45            
46            
47            
48            
49              
50             For more examples see F.
51              
52             =head1 WARNING
53              
54             experimental, use at your own risk :-)
55              
56             =head1 DESCRIPTION
57              
58             There are couple of modules mapping XML to data structures. (L,
59             L, L, ...) but they aim at making data structures
60             adapt to XML structure. This one defines couple of simple XML tags to represent
61             data structures. It makes the serialization to and from XML possible.
62              
63             For the moment it is an experiment. I plan to use it for passing data
64             structures as DOM to XSLT for transformations, so that I can match them
65             with XPATH similar way how I access them in Perl.
66              
67             /HASH/KEY[@name="key"]/VALUE
68             /HASH/KEY[@name="key2"]/ARRAY/*[3]/VALUE
69             /ARRAY/*[1]/VALUE
70             /ARRAY/*[2]/HASH/KEY[@name="key3"]/VALUE
71              
72             If you are looking for a module to serialize your data, without requirement
73             to do so in XML, you should probably better have a look at L
74             or L.
75              
76             =cut
77              
78 1     1   61184 use warnings;
  1         2  
  1         29  
79 1     1   5 use strict;
  1         1  
  1         50  
80              
81 1     1   6 use utf8;
  1         8  
  1         8  
82 1     1   54 use 5.010;
  1         4  
  1         37  
83 1     1   4 use feature 'state';
  1         3  
  1         107  
84              
85 1     1   6 use Carp 'croak';
  1         1  
  1         81  
86 1     1   1329 use XML::LibXML 'XML_ELEMENT_NODE';
  0            
  0            
87             use Scalar::Util 'blessed';
88             use URI::Escape qw(uri_escape uri_unescape);
89             use Test::Deep::NoTest 'eq_deeply';
90             use XML::Char;
91             use MIME::Base64 'decode_base64';
92              
93             our $VERSION = '0.07';
94              
95             use base 'Class::Accessor::Fast';
96              
97             =head1 PROPERTIES
98              
99             =over 4
100              
101             =item pretty
102              
103             (default 1 - true) will insert text nodes to the XML to make the output indented.
104              
105             =item safe_mode
106              
107             (default undef - false)
108              
109             in case of C perform the xml string decoding back and will compare
110             the two data structures to be sure the data can be reconstructed back without
111             errors.
112              
113             in case of a C it will decode to data then encode to xml string and from
114             xml string decode back to data. this two data values are then compared.
115              
116             Both compares is done using L::eq_deeply.
117              
118             =item namespace
119              
120             (default undef - false)
121              
122             adds xml:ns attribute to the root element. if C is set to 1
123             the xml:ns will be L otherwise
124             it will be the value of C.
125              
126             =back
127              
128             =cut
129              
130             __PACKAGE__->mk_accessors(qw{
131             pretty
132             safe_mode
133             namespace
134             namespace_prefix
135             });
136              
137             =head1 METHODS
138              
139             =head2 new()
140              
141             Object constructor.
142              
143             =cut
144              
145             sub new {
146             my $class = shift;
147             my $self = $class->SUPER::new({
148             'pretty' => 1,
149             @_
150             });
151            
152             return $self;
153             }
154              
155             sub _xml {
156             my($self) = @_;
157             if(not exists $self->{'_xml'}) {
158             my $xml = XML::LibXML::Document->new("1.0", "UTF-8");
159             $self->{'_xml'} = $xml;
160             }
161             return $self->{'_xml'};
162             }
163              
164              
165             sub _indent {
166             my $self = shift;
167             my $where = shift;
168             my $indent = shift;
169            
170             $where->addChild( $self->_xml->createTextNode( "\n".("\t" x $indent) ) )
171             if $self->pretty;
172             }
173              
174             sub _createElement {
175             my $self = shift;
176             my $name = shift;
177             my $namespace = $self->namespace;
178             my $namespace_prefix = $self->namespace_prefix;
179              
180             $name = join(':',$namespace_prefix,$name)
181             if $namespace_prefix;
182              
183             if ($namespace) {
184             return $self->_xml->createElementNS( $namespace, $name );
185             }
186             else {
187             return $self->_xml->createElement($name);
188             }
189             }
190              
191             =head2 encode($what)
192              
193             From structure C<$what> generates L DOM. Call
194             C<< ->toString >> to get XML string. For more actions see L.
195              
196             =cut
197              
198             sub encode {
199             my $self = shift;
200             my $what = shift;
201             my $pos = shift || 1;
202             my $where;
203            
204             my $safe_mode = $self->safe_mode;
205             $self->safe_mode(0);
206             my $add_namespace = $self->namespace || 0;
207             $add_namespace = "http://search.cpan.org/perldoc?Data::asXML"
208             if $add_namespace eq '1';
209             $self->namespace(0);
210             $self->namespace($add_namespace)
211             if $add_namespace;
212            
213             state $indent = 0;
214              
215             if (not $self->{'_cur_xpath_steps'}) {
216             $self->{'_href_mapping'} = {};
217             $self->{'_cur_xpath_steps'} = [];
218             }
219            
220             # create DOM for hash element
221             if (ref($what) eq 'HASH') {
222             $where = $self->_createElement('HASH');
223             $indent++;
224             push @{$self->{'_cur_xpath_steps'}}, $pos;
225             # already encoded reference
226             if (exists $self->{'_href_mapping'}->{$what}) {
227             $where->setAttribute(
228             'href' =>
229             $self->_make_relative_xpath(
230             [ split(',', $self->{'_href_mapping'}->{$what}) ],
231             $self->{'_cur_xpath_steps'}
232             )
233             );
234             $indent--;
235             pop @{$self->{'_cur_xpath_steps'}};
236             return $where;
237             }
238             $self->{'_href_mapping'}->{$what} = $self->_xpath_steps_string();
239            
240             my $key_pos = 0;
241             foreach my $key (sort keys %{$what}) {
242             my $value = $what->{$key};
243             $key_pos++;
244             $self->_indent($where, $indent);
245             $indent++;
246              
247             my $el = $self->_createElement('KEY');
248             push @{$self->{'_cur_xpath_steps'}}, $key_pos;
249             $self->_indent($el, $indent);
250             $el->setAttribute('name', $key);
251             $el->addChild($self->encode($value));
252              
253             $indent--;
254             $self->_indent($el, $indent);
255             pop @{$self->{'_cur_xpath_steps'}};
256              
257             $where->addChild($el);
258             }
259            
260             $indent--;
261             $self->_indent($where, $indent);
262             pop @{$self->{'_cur_xpath_steps'}};
263             }
264             # create DOM for array element
265             elsif (ref($what) eq 'ARRAY') {
266             $where = $self->_createElement('ARRAY');
267             $indent++;
268             push @{$self->{'_cur_xpath_steps'}}, $pos;
269             # already encoded reference
270             if (exists $self->{'_href_mapping'}->{$what}) {
271             $where->setAttribute(
272             'href' =>
273             $self->_make_relative_xpath(
274             [ split(',', $self->{'_href_mapping'}->{$what}) ],
275             $self->{'_cur_xpath_steps'}
276             )
277             );
278             $indent--;
279             pop @{$self->{'_cur_xpath_steps'}};
280             return $where;
281             }
282             $self->{'_href_mapping'}->{$what.''} = $self->_xpath_steps_string();
283            
284             my $array_pos = 0;
285             foreach my $value (@{$what}) {
286             $array_pos++;
287             $self->_indent($where, $indent);
288             $where->addChild($self->encode($value, $array_pos));
289             }
290            
291             $indent--;
292             $self->_indent($where, $indent);
293             pop @{$self->{'_cur_xpath_steps'}};
294             }
295             # create element for pure reference
296             elsif (ref($what) eq 'REF') {
297             $where = $self->_createElement('REF');
298             $indent++;
299             push @{$self->{'_cur_xpath_steps'}}, $pos;
300             # already encoded reference
301             if (exists $self->{'_href_mapping'}->{$what}) {
302             $where->setAttribute(
303             'href' =>
304             $self->_make_relative_xpath(
305             [ split(',', $self->{'_href_mapping'}->{$what}) ],
306             $self->{'_cur_xpath_steps'}
307             )
308             );
309             $indent--;
310             pop @{$self->{'_cur_xpath_steps'}};
311             return $where;
312             }
313             $self->{'_href_mapping'}->{$what.''} = $self->_xpath_steps_string();
314            
315             $self->_indent($where, $indent);
316             $where->addChild($self->encode($$what));
317            
318             $indent--;
319             $self->_indent($where, $indent);
320             pop @{$self->{'_cur_xpath_steps'}};
321             }
322             # scalar reference
323             elsif (ref($what) eq 'SCALAR') {
324             push @{$self->{'_cur_xpath_steps'}}, $pos;
325             # already encoded reference
326             if (exists $self->{'_href_mapping'}->{$what}) {
327             $where = $self->_createElement('VALUE');
328             $where->setAttribute(
329             'href' =>
330             $self->_make_relative_xpath(
331             [ split(',', $self->{'_href_mapping'}->{$what}) ],
332             $self->{'_cur_xpath_steps'}
333             )
334             );
335             pop @{$self->{'_cur_xpath_steps'}};
336             return $where;
337             }
338             $self->{'_href_mapping'}->{$what.''} = $self->_xpath_steps_string();
339              
340             $where = $self->encode($$what);
341             $where->setAttribute('subtype' => 'ref');
342              
343             pop @{$self->{'_cur_xpath_steps'}};
344             }
345             # create text node
346             elsif (ref($what) eq '') {
347             $where = $self->_createElement('VALUE');
348             if (defined $what) {
349             # uri escape if it contains invalid XML characters
350             if (not XML::Char->valid($what)) {
351             $what = join q(), map {
352             (/[[:^print:]]/ or q(%) eq $_) ? uri_escape $_ : $_
353             } split //, $what;
354             $where->setAttribute('type' => 'uriEscape');
355             }
356             $where->addChild( $self->_xml->createTextNode( $what ) );
357             }
358             else {
359             # no better way to distinguish between empty string and undef - see http://rt.cpan.org/Public/Bug/Display.html?id=51442
360             $where->setAttribute('type' => 'undef');
361             }
362            
363             }
364             #
365             else {
366             die 'unknown reference - '.$what;
367             }
368              
369             # cleanup at the end
370             if ($indent == 0) {
371             $self->{'_href_mapping'} = {};
372             $self->{'_cur_xpath_steps'} = [];
373             }
374              
375             # in safe_mode decode back the xml string and compare the data structures
376             if ($safe_mode) {
377             my $xml_string = $where->toString;
378             my $what_decoded = eval { $self->decode($xml_string) };
379            
380             die 'encoding failed '.$@.' of '.eval('use Data::Dumper; Dumper([$what, $xml_string, $what_decoded])').' failed'
381             if not eq_deeply($what, $what_decoded);
382            
383             # set back the safe mode after all was encoded
384             $self->safe_mode($safe_mode);
385             }
386              
387             return $where;
388             }
389              
390             sub _xpath_steps_string {
391             my $self = shift;
392             my $path_array = shift || $self->{'_cur_xpath_steps'};
393             return join(',',@{$path_array});
394             }
395              
396             sub _make_relative_xpath {
397             my $self = shift;
398             my $orig_path = shift;
399             my $cur_path = shift;
400            
401             # find how many elements (from beginning) the paths are sharing
402             my $common_root_index = 0;
403             while (
404             ($common_root_index < @$orig_path)
405             and ($orig_path->[$common_root_index] == $cur_path->[$common_root_index])
406             ) {
407             $common_root_index++;
408             }
409            
410             # add '..' to move up the element hierarchy until the common element
411             my @rel_path = ();
412             my $i = $common_root_index+1;
413             while ($i < scalar @$cur_path) {
414             push @rel_path, '..';
415             $i++;
416             }
417            
418             # add the original element path steps
419             $i = $common_root_index;
420             while ($i < scalar @$orig_path) {
421             push @rel_path, $orig_path->[$i];
422             $i++;
423             }
424            
425             # in case of self referencing the element index is needed
426             if ($i == $common_root_index) {
427             push @rel_path, '..', $orig_path->[-1];
428             }
429            
430             # return relative xpath
431             return join('/', map { $_ eq '..' ? $_ : '*['.$_.']' } @rel_path);
432             }
433              
434             =head2 decode($xmlstring)
435              
436             Takes C<$xmlstring> and converts to data structure.
437              
438             =cut
439              
440             sub decode {
441             my $self = shift;
442             my $xml = shift;
443             my $pos = shift || 1;
444              
445             # in safe_mode "encode+decode" the decoded data for comparing
446             if ($self->safe_mode) {
447             $self->safe_mode(0);
448             my $data = $self->decode($xml, $pos);
449             my $data_redecoded = eval { $self->decode(
450             $self->encode($data)->toString,
451             $pos,
452             )};
453             die 'redecoding failed "'.$@.'" of '.eval('use Data::Dumper; Dumper([$xml, $data, $data_redecoded])').' failed'
454             if not eq_deeply($data, $data_redecoded);
455             $self->safe_mode(1);
456             return $data;
457             }
458              
459             if (not $self->{'_cur_xpath_steps'}) {
460             local $self->{'_href_mapping'} = {};
461             local $self->{'_cur_xpath_steps'} = [];
462             }
463              
464             my $value;
465            
466             if (not blessed $xml) {
467             my $parser = XML::LibXML->new();
468             my $doc = $parser->parse_string($xml);
469             my $root_element = $doc->documentElement();
470            
471             return $self->decode($root_element);
472             }
473            
474             if ($xml->nodeName eq 'HASH') {
475             if (my $xpath_path = $xml->getAttribute('href')) {
476             my $href_key = $self->_href_key($xpath_path);
477             return $self->{'_href_mapping'}->{$href_key} || die 'invalid reference - '.$href_key.' ('.$xml->toString.')';
478             }
479            
480             push @{$self->{'_cur_xpath_steps'}}, $pos;
481            
482             my %data;
483             $self->{'_href_mapping'}->{$self->_xpath_steps_string()} = \%data;
484             my @keys =
485             grep { $_->nodeName eq 'KEY' }
486             grep { $_->nodeType eq XML_ELEMENT_NODE }
487             $xml->childNodes()
488             ;
489             my $key_pos = 1;
490             foreach my $key (@keys) {
491             push @{$self->{'_cur_xpath_steps'}}, $key_pos;
492             my $key_name = $key->getAttribute('name');
493             my $key_value = $self->decode(grep { $_->nodeType eq XML_ELEMENT_NODE } $key->childNodes()); # is always only one
494             $data{$key_name} = $key_value;
495             pop @{$self->{'_cur_xpath_steps'}};
496             $key_pos++;
497             }
498             pop @{$self->{'_cur_xpath_steps'}};
499             return \%data;
500             }
501             elsif ($xml->nodeName eq 'ARRAY') {
502             if (my $xpath_path = $xml->getAttribute('href')) {
503             my $href_key = $self->_href_key($xpath_path);
504            
505             return $self->{'_href_mapping'}->{$href_key} || die 'invalid reference - '.$href_key.' ('.$xml->toString.')';
506             }
507              
508             push @{$self->{'_cur_xpath_steps'}}, $pos;
509              
510             my @data;
511             $self->{'_href_mapping'}->{$self->_xpath_steps_string()} = \@data;
512            
513             my $array_element_pos = 1;
514             @data = map { $self->decode($_, $array_element_pos++) } grep { $_->nodeType eq XML_ELEMENT_NODE } $xml->childNodes();
515             pop @{$self->{'_cur_xpath_steps'}};
516             return \@data;
517             }
518             elsif ($xml->nodeName eq 'REF') {
519             if (my $xpath_path = $xml->getAttribute('href')) {
520             my $href_key = $self->_href_key($xpath_path);
521             return $self->{'_href_mapping'}->{$href_key} || die 'invalid reference - '.$href_key.' ('.$xml->toString.')';
522             }
523              
524             push @{$self->{'_cur_xpath_steps'}}, $pos;
525              
526             my $data;
527             $self->{'_href_mapping'}->{$self->_xpath_steps_string()} = \$data;
528            
529             ($data) = map { $self->decode($_) } grep { $_->nodeType eq XML_ELEMENT_NODE } $xml->childNodes();
530              
531             pop @{$self->{'_cur_xpath_steps'}};
532             return \$data;
533             }
534             elsif ($xml->nodeName eq 'VALUE') {
535             if (my $xpath_path = $xml->getAttribute('href')) {
536             my $href_key = $self->_href_key($xpath_path);
537             return $self->{'_href_mapping'}->{$href_key} || die 'invalid reference - '.$href_key.' ('.$xml->toString.')';
538             }
539              
540             push @{$self->{'_cur_xpath_steps'}}, $pos;
541             my $value;
542             $self->{'_href_mapping'}->{$self->_xpath_steps_string()} = \$value;
543             pop @{$self->{'_cur_xpath_steps'}};
544            
545             my $type = $xml->getAttribute('type') // '';
546             my $subtype = $xml->getAttribute('subtype') // '';
547             if ($type eq 'undef')
548             { $value = undef; }
549             elsif ($type eq 'base64')
550             { $value = decode_base64($xml->textContent) } # left for backwards compatibility, will be removed one day...
551             elsif ($type eq 'uriEscape')
552             { $value = uri_unescape $xml->textContent; }
553             else
554             { $value = $xml->textContent }
555             return \$value
556             if ($subtype eq 'ref');
557             return $value;
558             }
559             else {
560             die 'invalid (unknown) element "'.$xml->toString.'"'
561             }
562            
563             }
564              
565             sub _href_key {
566             my $self = shift;
567             my $xpath_steps_string = shift;
568            
569             my @path = @{$self->{'_cur_xpath_steps'}};
570             my @xpath_steps =
571             map { $_ =~ m/^\*\[(\d+)\]$/xms ? $1 : $_ }
572             split('/', $xpath_steps_string)
573             ;
574            
575             my $i = 0;
576             while ($i < @xpath_steps) {
577             if ($xpath_steps[$i] eq '..') {
578             pop(@path);
579             }
580             else {
581             push(@path, $xpath_steps[$i]);
582             }
583             $i++;
584             }
585             return $self->_xpath_steps_string(\@path)
586             }
587              
588             1;
589              
590              
591             __END__