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