File Coverage

blib/lib/SOAP/Data/ComplexType.pm
Criterion Covered Total %
statement 34 195 17.4
branch 0 122 0.0
condition 0 24 0.0
subroutine 12 26 46.1
pod n/a
total 46 367 12.5


line stmt bran cond sub pod time code
1             package SOAP::Data::ComplexType;
2             our $VERSION = 0.044;
3              
4 5     5   160459 use strict;
  5         13  
  5         145  
5 5     5   26 use warnings;
  5         8  
  5         114  
6 5     5   25 use Carp ();
  5         7  
  5         117  
7 5     5   28 use Scalar::Util;
  5         8  
  5         2392  
8              
9              
10 5     5   30 use constant OBJ_URI => undef;
  5         10  
  5         4325  
11 5     5   28 use constant OBJ_TYPE => undef; #format: ns:type
  5         9  
  5         238  
12 5     5   31 use constant OBJ_FIELDS => {}; #format: name=>[type, uri, attr]
  5         7  
  5         235  
13              
14 5     5   25 use vars qw($AUTOLOAD);
  5         7  
  5         11780  
15              
16             sub new {
17 0     0     my $proto = shift;
18 0   0       my $class = ref($proto) || $proto;
19 0           my $data = shift; #can be HASH ref or SOAP::SOM->result object
20 0           my $obj_fields = shift; #href: name=>[(scalar)type, (href)attr]; or name=>[[(scalar)type, href], (href)attr]; or name=>[[(scalar)type, [(scalar)type, href]], (href)attr]; ...
21 0           my $self = { _sdb_obj => SOAP::Data::ComplexType::Builder->new(readable=>1) };
22 0           bless($self, $class);
23 0           my $data_in = $self->_convert_object_to_raw($data);
24 0           $self->_parse_obj_fields($data_in, $obj_fields, undef);
25 0           return $self;
26             }
27              
28             sub _convert_object_to_raw { #recursive method: convert any object elements into perl primitives
29 0     0     my $self = shift;
30 0           my $obj = shift;
31 0           my $ancestors = shift;
32            
33 0           my $addr = Scalar::Util::refaddr($obj);
34 0 0         if (defined $ancestors) {
35 0 0         if (grep(/^$addr$/, @{$ancestors})) {
  0            
36 0           warn "Recursive processing halted: Circular reference with ancestor $addr detected\n";
37 0           return undef;
38             }
39 0           push @{$ancestors}, $addr;
  0            
40             }
41             else {
42 0           $ancestors = [$addr];
43             }
44              
45 0           my $ret;
46 0 0         if (UNIVERSAL::isa($obj, 'Array')) { #special case: complex type Array is stored as a hash, needs conversion to native perl
    0          
    0          
    0          
47 0 0         push @{$ret}, ref($obj->{$_}) ? $self->_convert_object_to_raw($obj->{$_}, $ancestors) : $obj->{$_} foreach (keys %{$obj});
  0            
  0            
48             }
49             elsif (UNIVERSAL::isa($obj, 'HASH')) {
50 0 0         $ret->{$_} = ref($obj->{$_}) ? $self->_convert_object_to_raw($obj->{$_}, $ancestors) : $obj->{$_} foreach (keys %{$obj});
  0            
51             }
52             elsif (UNIVERSAL::isa($obj, 'ARRAY')) {
53 0 0         push @{$ret}, ref($_) ? $self->_convert_object_to_raw($_, $ancestors) : $_ foreach (@{$obj});
  0            
  0            
54             }
55             elsif (UNIVERSAL::isa($obj, 'SCALAR')) { #future: do we *really* want to deref scalarref?
56 0 0         $ret = ref(${$obj}) ? $self->_convert_object_to_raw(${$obj}, $ancestors) : ${$obj};
  0            
  0            
  0            
57             }
58             else { #base case
59 0           $ret = $obj;
60             }
61 0           return $ret;
62             }
63              
64             sub _parse_obj_fields { #recursive method
65 0     0     my $self = shift;
66 0           my $data = shift;
67 0           my $obj_fields = shift;
68 0           my $parent_obj = shift;
69 0           my $parent_obj_is_arraytype = shift;
70              
71             ### validate parameters ###
72 0 0         unless (UNIVERSAL::isa($data, 'HASH')) {
73 0           Carp::confess "Input data not expected ref type: HASH";
74             }
75 0 0 0       unless (UNIVERSAL::isa($obj_fields, 'HASH') && scalar keys %{$obj_fields} > 0) {
  0            
76 0           Carp::confess "Object field definitions invalid or undefined.";
77             }
78              
79             ### generate data structures ###
80 0           foreach my $key (keys %{$obj_fields}) {
  0            
81 0           my $key_regex = quotemeta $key;
82 0 0         if ($parent_obj_is_arraytype) { #array special case: define child object that becomes parent of array values
  0 0          
83 0           my ($type, $uri, $attributes) = @{$obj_fields->{$key}};
  0            
84 0           my $value = $data;
85             # if ($required) {
86             # Carp::cluck "Warning: Required field '$key' is null" && next unless (UNIVERSAL::isa($value, 'HASH') && scalar keys %{$value}) || (UNIVERSAL::isa($value, 'ARRAY') && @{$value});
87             # }
88 0           my ($c_type, $c_fields);
89 0 0         if (UNIVERSAL::isa($type, 'ARRAY')) {
90 0           ($c_type, $c_fields) = @{$type};
  0            
91             }
92 0 0         my $obj = $self->{_sdb_obj}->add_elem(
93             name => $key,
94             value => undef,
95             type => defined $c_type ? $c_type : $type, #if array of complex type, else array of simple type
96             uri => $uri,
97             attributes => $attributes,
98             parent => $parent_obj
99             );
100 0 0         my @values = UNIVERSAL::isa($value, 'ARRAY') ? @{$value} : ($value);
  0            
101 0           foreach my $val (@values) {
102 0 0         if (UNIVERSAL::isa($type, 'ARRAY')) { #recursion case: complex subtype up to N levels deep
103 0 0         if (UNIVERSAL::isa($val, 'HASH')) { $self->_parse_obj_fields($val, $c_fields, $obj, $c_type =~ m/(^|.+:)Array$/o ? 1 : 0); }
  0 0          
104 0 0         else { Carp::cluck "Warning: Expected hash ref value for key '$key', found scalar. Ignoring data value '$val'" if defined $val; }
105             }
106             else { #base case
107 0           $self->{_sdb_obj}->add_elem(
108             name => $key,
109             value => $val,
110             type => $type,
111             uri => $uri,
112             attributes => $attributes,
113             parent => $obj
114             );
115             }
116             }
117             }
118             elsif (grep(/^$key_regex$/, keys %{$data})) { #base object processing
119 0           my ($type, $uri, $attributes) = @{$obj_fields->{$key}};
  0            
120 0           my $value = $data->{$key};
121             # if ($required) {
122             # Carp::cluck "Warning: Required field '$key' is null" && next unless (UNIVERSAL::isa($value, 'HASH') && scalar keys %{$value}) || (UNIVERSAL::isa($value, 'ARRAY') && @{$value});
123             # }
124 0 0         if (UNIVERSAL::isa($type, 'ARRAY')) {
125 0           my ($c_type, $c_fields) = @{$type};
  0            
126 0           my $array_obj;
127 0 0         if ($c_type =~ m/(^|.+:)Array$/o) { #complex array
128 0           $array_obj = $self->{_sdb_obj}->add_elem(
129             name => $key,
130             value => undef,
131             type => $c_type,
132             uri => $uri,
133             attributes => $attributes,
134             parent => $parent_obj
135             );
136             }
137 0 0         my @values = UNIVERSAL::isa($value, 'ARRAY') ? @{$value} : ($value);
  0            
138 0           foreach my $val (@values) {
139 0 0         my $obj = $c_type =~ m/(^|.+:)Array$/o
140             ? $array_obj #complex array
141             : $self->{_sdb_obj}->add_elem( #simple array
142             name => $key,
143             value => undef,
144             type => $c_type,
145             uri => $uri,
146             attributes => $attributes,
147             parent => $parent_obj
148             );
149             #warn "Added element $key\n";
150 0 0         if (UNIVERSAL::isa($val, 'HASH')) { $self->_parse_obj_fields($val, $c_fields, $obj, $c_type =~ m/(^|.+:)Array$/o ? 1 : 0); }
  0 0          
151 0 0         else { Carp::cluck "Warning: Expected hash ref value for key '$key', found scalar. Ignoring data value '$val'" if defined $val; }
152             }
153             }
154             else { #base case
155             # if ($required) {
156             # Carp::cluck "Warning: Required field '$key' is null" && next unless defined $value;
157             # }
158 0 0         my @values = UNIVERSAL::isa($value, 'ARRAY') ? @{$value} : ($value);
  0            
159             $self->{_sdb_obj}->add_elem(
160             name => $key,
161             value => $_,
162             type => $type,
163             uri => $uri,
164             attributes => $attributes,
165             parent => $parent_obj
166 0           ) foreach (@values);
167             #warn "Added element $key=$value\n";
168             }
169             }
170             }
171             }
172              
173 0     0     sub DESTROY {}
174 0     0     sub CLONE {}
175              
176             sub AUTOLOAD {
177 0     0     my $self = shift;
178 0   0       my $class = ref($self) || Carp::confess "'$self' is not an object";
179 0           my $name = $AUTOLOAD;
180 0           my $value = shift;
181 0           $name =~ s/.*://o; # strip fully-qualified portion
182 0           my $elem;
183 0 0         my @res = defined $value ? $self->set($name, $value) : $self->get($name);
184 0 0         return wantarray ? @res : $res[0];
185             }
186              
187             sub get_elem {
188 0     0     my $self = shift;
189 0   0       my $class = ref($self) || Carp::confess "'$self' is not an object";
190 0           my $name = shift;
191 0           my $elem;
192 0 0         unless (defined ($elem = $self->{_sdb_obj}->get_elem($name))) {
193 0           Carp::cluck "Can't access '$name' element object in class $class";
194             }
195 0           return $elem;
196             }
197              
198             sub get {
199 0     0     my $self = shift;
200 0   0       my $class = ref($self) || Carp::confess "'$self' is not an object";
201 0           my $name = shift;
202 0           my $elem;
203 0 0         return wantarray ? () : undef unless defined ($elem = $self->get_elem($name));
    0          
204 0           my $res = $elem->value();
205 0 0         if ($elem->{type} =~ m/(^|.+:)Array$/o) {
206 0 0         return wantarray ? @{$res} : scalar @{$res} if defined $res;
  0 0          
  0            
207 0 0         return wantarray ? () : 0;
208             }
209             else {
210 0 0         return defined $res ? $res->[0] : undef;
211             }
212             }
213              
214             sub set {
215 0     0     my $self = shift;
216 0   0       my $class = ref($self) || Carp::confess "'$self' is not an object";
217 0           my $name = shift;
218 0           my $value = shift;
219            
220             ### validate input is valid object or list of objects ###
221 0 0         if (ref $value) {
222 0 0         if (ref($value) eq 'ARRAY') {
223 0           foreach (@{$value}) {
  0            
224 0 0 0       Carp::cluck "Value ".ref($_)." is not a valid SOAP::Data::ComplexType::Builder::Element object" if ref($_) && UNIVERSAL::isa($_, 'SOAP::Data::ComplexType::Builder::Element');
225 0 0         return wantarray ? () : undef;
226             }
227             } else {
228 0 0         Carp::cluck "Value ".ref($_)." is not a valid SOAP::Data::ComplexType::Builder::Element object" unless UNIVERSAL::isa($value, 'SOAP::Data::ComplexType::Builder::Element');
229 0 0         return wantarray ? () : undef;
230             }
231             }
232            
233 0           my $elem;
234 0 0         return wantarray ? () : undef unless ($elem = $self->get_elem($name));
    0          
235 0 0         my $res = $elem->value(ref($value) eq 'ARRAY' ? $value : [$value]);
236 0 0         if ($elem->{type} =~ m/(^|.+:)Array$/o) {
237 0 0         return wantarray ? @{$res} : scalar @{$res} if defined $res;
  0 0          
  0            
238 0 0         return wantarray ? () : 0;
239             }
240             else {
241 0 0         return defined $res ? $res->[0] : undef;
242             }
243             }
244              
245             sub as_soap_data {
246 0     0     my $self = shift;
247 0 0         return @_ ? $self->{_sdb_obj}->get_elem($_[0])->get_as_data : $self->{_sdb_obj}->to_soap_data;
248             }
249              
250             sub as_soap_data_instance {
251 0     0     my $self = shift;
252 0           my $class = ref($self);
253 0           my %args = @_;
254 5     5   37 no strict 'refs';
  5         8  
  5         4205  
255 0           return SOAP::Data->new(
256             exists $args{name} ? (name => $args{name}) : (),
257 0           type => exists $args{type} ? $args{type} : &{"$class\::OBJ_TYPE"},
258 0 0         uri => exists $args{uri} ? $args{uri} : &{"$class\::OBJ_URI"},
    0          
    0          
    0          
259             attr => exists $args{attr} ? $args{attr} : {},
260             value => \SOAP::Data->value($self->as_soap_data)
261             );
262             }
263              
264             sub as_xml_data {
265 0     0     return shift->{_sdb_obj}->serialise(@_);
266             }
267              
268             sub as_xml_data_instance {
269 0     0     my $self = shift;
270 0           my $serialized = SOAP::Serializer->autotype($self->{_sdb_obj}->autotype)->readable($self->{_sdb_obj}->readable)->serialize( $self->as_soap_data_instance(@_) );
271             }
272              
273             sub as_raw_data {
274 0     0     my $self = shift;
275 0           my $data;
276 0 0         if (@_) {
277 0           $data = eval { $self->{_sdb_obj}->get_elem($_[0])->get_as_raw; };
  0            
278 0 0         warn $@ if $@;
279 0 0 0       $data = $data->{(keys %{$data})[0]} if ref($data) eq 'HASH' && scalar keys %{$data} == 1; #remove parent key in this case
  0            
  0            
280             }
281             else {
282 0           $data = $self->{_sdb_obj}->to_raw_data;
283             }
284 0           return $data;
285             }
286              
287             package SOAP::Data::ComplexType::Builder;
288             #adds type, uri field to Builder object
289              
290 5     5   69 use strict;
  5         10  
  5         156  
291 5     5   24 use warnings;
  5         8  
  5         173  
292 5     5   11664 use SOAP::Data::Builder 0.8;
  0            
  0            
293             use vars qw(@ISA);
294             @ISA = qw(SOAP::Data::Builder);
295              
296             sub new {
297             my $proto = shift;
298             my $class = ref($proto) || $proto;
299             my $self = $class->SUPER::new(@_);
300             return bless($self, $class);
301             }
302              
303             sub add_elem {
304             my ($self,%args) = @_;
305             my $elem = SOAP::Data::ComplexType::Builder::Element->new(%args);
306             if ( defined $args{parent} ) {
307             my $parent = $args{parent};
308             unless (UNIVERSAL::isa($parent, 'SOAP::Data::ComplexType::Builder::Element')) {
309             $parent = $self->get_elem($args{parent});
310             }
311             $parent->add_elem($elem);
312             } else {
313             push(@{$self->{elements}},$elem);
314             }
315             return $elem;
316             }
317              
318             sub find_elem {
319             my ($self,$elem,$key,@keys) = @_;
320             return UNIVERSAL::isa($elem, 'SOAP::Data::ComplexType::Builder::Element') ? $elem->find_elem($key,@keys) : undef;
321             }
322              
323             sub get_as_data {
324             my $self = shift;
325             my $elem = shift;
326             return UNIVERSAL::isa($elem, 'SOAP::Data::ComplexType::Builder::Element') ? $elem->get_as_data() : undef;
327             }
328              
329             sub to_raw_data {
330             my $self = shift;
331             my @data = ();
332             foreach my $elem ( $self->elems ) {
333             my $raw = $self->get_as_raw($elem);
334             push(@data,ref($raw) eq 'HASH' ? %{$raw} : ref($raw) eq 'ARRAY' ? @{$raw} : $raw);
335             }
336             return {@data};
337             }
338              
339             sub get_as_raw {
340             my $self = shift;
341             my $elem = shift;
342             return UNIVERSAL::isa($elem, 'SOAP::Data::ComplexType::Builder::Element') ? $elem->get_as_raw() : undef;
343             }
344              
345             sub serialise {
346             my $self = shift;
347             my $data = @_
348             ? eval { SOAP::Data->value( $self->get_elem($_[0])->get_as_data ); }
349             : SOAP::Data->name('SOAP:ENV' => \SOAP::Data->value( $self->to_soap_data ) );
350             warn $@ if $@;
351             my $serialized = SOAP::Serializer->autotype($self->autotype)->readable($self->readable)->serialize( $data );
352             }
353              
354             package SOAP::Data::ComplexType::Builder::Element;
355             #supports type and uri; correctly handles '0' data value
356              
357             use strict;
358             use warnings;
359             use SOAP::Data::Builder::Element;
360             use vars qw(@ISA);
361             @ISA = qw(SOAP::Data::Builder::Element);
362             use Carp ();
363             use Scalar::Util;
364              
365             use vars qw($AUTOLOAD);
366              
367             sub new {
368             my ($class,%args) = @_;
369             my $self = {};
370             bless ($self,ref $class || $class);
371             foreach my $key (keys %args) {
372             $self->{lc $key} = defined $args{$key} ? $args{$key} : undef;
373             }
374             if ($args{parent}) {
375             Scalar::Util::weaken($self->{parent}) if ref $args{parent};
376             $self->{fullname} = (ref $args{parent} ? $args{parent}->{fullname} : $args{parent})."/$args{name}";
377             }
378             $self->{fullname} ||= $args{name};
379             $self->{VALUE} = defined $args{value} ? [ $args{value} ] : [];
380             return $self;
381             }
382              
383             sub DESTROY {}
384             sub CLONE {}
385              
386             sub AUTOLOAD {
387             my $self = shift;
388             my $class = ref($self) || Carp::confess "'$self' is not an object";
389             my $name = $AUTOLOAD;
390             my $value = shift;
391             $name =~ s/.*://o; # strip fully-qualified portion
392             my $elem;
393             my @res = defined $value ? $self->set($name, $value) : $self->get($name);
394             return wantarray ? @res : $res[0];
395             }
396              
397             sub get_elem {
398             my ($self,$name) = (@_,'');
399             my ($a,$b);
400             my @keys = split (/\//,$name);
401             foreach my $elem ( $self->get_children()) {
402             next unless ref $elem;
403             if ($elem->name eq $keys[0]) {
404             $a = $elem;
405             $b = shift(@keys);
406             last;
407             }
408             }
409              
410             Carp::cluck "Can't access '$name' element object in class ".ref($self) unless defined $a;
411             my $elem = $a;
412             $b = shift(@keys);
413             if ($b) {
414             $elem = $elem->find_elem($b,@keys);
415             }
416            
417             Carp::cluck "Can't access '$name' element object in class ".ref($self) unless defined $elem;
418             return $elem;
419             }
420              
421             sub find_elem {
422             my ($self,$key,@keys) = @_;
423             my ($a,$b);
424             foreach my $elem ( $self->get_children()) {
425             next unless ref $elem;
426             if ($elem->{name} eq $key) {
427             $a = $elem;
428             $b = $key;
429             last;
430             }
431             }
432              
433             my $elem = $a;
434             undef($b);
435             while ($b = shift(@keys) ) {
436             $elem = $elem->find_elem($b,@keys);
437             }
438             return $elem;
439             }
440              
441             sub get {
442             my $self = shift;
443             my $class = ref($self) || Carp::confess "'$self' is not an object";
444             my $name = shift;
445             my $elem;
446             return wantarray ? () : undef unless defined ($elem = $self->get_elem($name));
447             my $res = $elem->value();
448             if ($elem->{type} =~ m/(^|.+:)Array$/o) {
449             return wantarray ? @{$res} : scalar @{$res} if defined $res;
450             return wantarray ? () : 0;
451             }
452             else {
453             return defined $res ? $res->[0] : undef;
454             }
455             }
456              
457             sub set {
458             my $self = shift;
459             my $class = ref($self) || Carp::confess "'$self' is not an object";
460             my $name = shift;
461             my $value = shift;
462            
463             ### validate input is valid object or list of objects ###
464             if (ref $value) {
465             if (ref($value) eq 'ARRAY') {
466             foreach (@{$value}) {
467             Carp::cluck "Value ".ref($_)." is not a valid SOAP::Data::ComplexType::Builder::Element object" if ref($_) && UNIVERSAL::isa($_, 'SOAP::Data::ComplexType::Builder::Element');
468             return wantarray ? () : undef;
469             }
470             } else {
471             Carp::cluck "Value ".ref($_)." is not a valid SOAP::Data::ComplexType::Builder::Element object" unless UNIVERSAL::isa($value, 'SOAP::Data::ComplexType::Builder::Element');
472             return wantarray ? () : undef;
473             }
474             }
475            
476             my $elem;
477             return wantarray ? () : undef unless ($elem = $self->get_elem($name));
478             my $res = $elem->value(ref($value) eq 'ARRAY' ? $value : [$value]);
479             if ($elem->{type} =~ m/(^|.+:)Array$/o) {
480             return wantarray ? @{$res} : scalar @{$res} if defined $res;
481             return wantarray ? () : 0;
482             }
483             else {
484             return defined $res ? $res->[0] : undef;
485             }
486             }
487              
488             sub add_elem {
489             my $self = shift;
490             my $elem;
491             if (UNIVERSAL::isa($_[0], __PACKAGE__)) {
492             $elem = $_[0];
493             push(@{$self->{VALUE}},$elem);
494             } else {
495             my $class = ref $self;
496             push(@{$self->{VALUE}},$class->new(@_));
497             }
498             return $elem;
499             }
500              
501             sub name {
502             my $self = shift;
503             my $value = shift;
504             if (defined $value) {
505             $self->{name} = $value;
506             } else {
507             $value = $self->{name};
508             }
509             return $value;
510             }
511              
512             sub value {
513             my $self = shift;
514             my $value = shift;
515             my $last_value;
516             if (defined $value) {
517             if (ref $value) {
518             $last_value = $self->{VALUE};
519             $self->{VALUE} = $value;
520             } else {
521             $last_value = $self->{VALUE};
522             $self->{VALUE} = defined $value ? [$value] : [];
523             }
524             } else {
525             $last_value = $value = $self->{VALUE};
526             }
527             return $last_value;
528             }
529              
530             sub get_as_data {
531             my $self = shift;
532             my @values;
533             foreach my $value ( @{$self->{VALUE}} ) {
534             next unless (defined $value);
535             if (ref $value) {
536             push(@values,$value->get_as_data())
537             } else {
538             push(@values,$value);
539             }
540             }
541              
542             my @data = ();
543              
544             if (ref $values[0]) {
545             $data[0] = \SOAP::Data->value( @values );
546             } else {
547             @data = @values;
548             }
549              
550             my %attributes = %{$self->attributes()};
551             my $arrayTypeAttr = (grep(/(^|.+:)arrayType$/, keys %attributes))[0];
552             $attributes{$arrayTypeAttr} = $attributes{$arrayTypeAttr}.'['.(scalar @values).']' if defined $arrayTypeAttr;
553             if ($self->{header}) {
554             $data[0] = SOAP::Header->name($self->{name} => $data[0])->attr(\%attributes)->type($self->{type})->uri($self->{uri});
555             } else {
556             if ($self->{isMethod}) {
557             @data = ( SOAP::Data->name($self->{name})->attr(\%attributes)->type($self->{type})->uri($self->{uri})
558             => SOAP::Data->value(@values)->type($self->{type})->uri($self->{uri}) );
559             } else {
560             $data[0] = SOAP::Data->name($self->{name} => $data[0])->attr(\%attributes)->type($self->{type})->uri($self->{uri});
561             }
562             }
563              
564             return @data;
565             }
566              
567             sub get_as_raw {
568             my $self = shift;
569             my $is_parent_arraytype = shift;
570             my @values;
571             foreach my $value ( @{$self->{VALUE}} ) {
572             if (ref $value) { #ref => object
573             push(@values,$value->get_as_raw($self->{type} =~ m/(^|.+:)Array$/o ? 1 : 0))
574             } else {
575             push(@values,$value);
576             }
577             }
578             push @values, undef unless @values; #insure undef value has the value undef
579             my $data;
580             if ($self->{type} =~ m/(^|.+:)Array$/o) {
581             $data->{$self->{name}} = \@values;
582             }
583             else {
584             foreach my $value (@values) {
585             if ($is_parent_arraytype) {
586             if (ref $value eq 'HASH') {
587             $data->{$_} = $value->{$_} foreach keys %{$value};
588             } else {
589             $data = $value;
590             }
591             } else {
592             if (ref $value eq 'HASH') {
593             $data->{$self->{name}}->{$_} = $value->{$_} foreach keys %{$value};
594             } else {
595             $data->{$self->{name}} = $value;
596             }
597             }
598             }
599             }
600              
601             return $data;
602             }
603              
604             1;
605              
606             __END__