File Coverage

blib/lib/DAIA/Object.pm
Criterion Covered Total %
statement 27 245 11.0
branch 0 164 0.0
condition 0 55 0.0
subroutine 9 24 37.5
pod 10 11 90.9
total 46 499 9.2


line stmt bran cond sub pod time code
1 1     1   6 use strict;
  1         2  
  1         67  
2 1     1   6 use warnings;
  1         1  
  1         53  
3             package DAIA::Object;
4             #ABSTRACT: Abstract base class of all DAIA classes
5             our $VERSION = '0.43'; #VERSION
6              
7 1     1   852 use Carp::Clan;
  1         4774  
  1         6  
8 1     1   1089 use Data::Validate::URI qw(is_uri is_web_uri);
  1         63982  
  1         76  
9 1     1   1028 use IO::Scalar;
  1         13508  
  1         58  
10 1     1   9 use Scalar::Util qw(refaddr reftype);
  1         2  
  1         65  
11 1     1   1386 use JSON;
  1         13110  
  1         6  
12              
13             our $AUTOLOAD;
14             our @HIDDEN_PROPERTIES =
15             qw(to format xmlns cgi header xmlheader xslt pi callback exitif noutf8);
16              
17              
18             sub new {
19 0     0 1   my $class = shift;
20 0           my $self = bless { }, $class;
21              
22 0           my %hash;
23 0 0 0       if ( @_ == 1 and ref($_[0]) eq 'HASH' ) {
    0 0        
24 0           %hash = %{$_[0]};
  0            
25             } elsif ( @_ == 1 and ref($_[0]) eq $class ) {
26 0           %hash = %{$_[0]->struct}; # copy constructor
  0            
27             } else {
28 0           %hash = $self->_buildargs(@_);
29             }
30              
31             # abstract class handling
32 0 0         if ( $class eq 'DAIA::Availability' ) {
33 0 0         croak "Availability status missing" unless exists $hash{status};
34 0           $self->status( $hash{status} );
35 0           delete $hash{status};
36 0           $class = ref($self);
37             }
38              
39 0           my %hidden;
40 0           foreach ( @HIDDEN_PROPERTIES ) {
41 0 0         next unless exists $hash{$_};
42 0           $hidden{$_} = $hash{$_};
43 0           delete $hash{$_};
44             }
45 0 0         $self->{_hidden} = \%hidden if %hidden;
46              
47 1     1   331 no strict 'refs'; ##no critic
  1         1  
  1         1270  
48 0           my $PROPERTIES = \%{$class."::PROPERTIES"};
  0            
49 0           foreach my $property (keys %{$PROPERTIES}) {
  0            
50 0 0         $self->$property( undef ) unless exists $hash{$property};
51             }
52              
53 0           foreach my $property (keys %hash) {
54 0           $self->$property( $hash{$property} );
55             }
56              
57             #use Data::Dumper; print Dumper($self)."\n";
58 0           return $self;
59             }
60              
61              
62             sub add {
63 0     0 1   my $self = shift;
64              
65             #print "APPEND: " . ref($self) . "\n";
66              
67 0           foreach my $value (@_) {
68 0 0         next unless defined $value; # ignore undefined values
69              
70             #print "- " . ref($value) . "\n";
71              
72 0 0         croak "Cannot add $value to " . ref($self)
73             unless ref($value) =~ /^DAIA::([A-Z][a-z]+)$/;
74 0           my $property = lc($1);
75            
76             #no strict 'refs';
77             #my $PROPERTIES = \%{$class."::PROPERTIES"};
78            
79             # repeatable
80 0 0         if ( ref($self->{$property}) eq 'ARRAY' ) {
81 0           push @{$self->{$property}}, $value;
  0            
82             } else {
83 0           $self->$property( $value );
84             }
85             }
86             }
87              
88              
89             sub xml {
90 0     0 1   my ($self, %param) = @_;
91 0           $self->_hidden_prop( \%param );
92              
93 0   0       my $xmlns = $param{xmlns} || ($param{xslt} or $param{header});
94 0   0       my $pi = $param{pi} || [ ];
95 0 0 0       $pi = [$pi] unless (reftype($pi) || '') eq 'ARRAY';
96              
97 0 0         push @$pi, 'xml-stylesheet type="text/xsl" href="' . xml_escape_value($param{xslt}) . '"'
98             if $param{xslt};
99 0 0         @$pi = map { $_ =~ /^<\?.*\?>$/ ? "$_\n" : "\n" } @$pi;
  0            
100              
101 0           my $name = lc(ref($self));
102 0           $name =~ s/^daia:://;
103 0 0         $name = 'daia' if $name eq 'response';
104              
105 0           my $struct = $self->struct;
106 0 0         $struct->{xmlns} = "http://ws.gbv.de/daia/" if $xmlns;
107 0           my $xml = xml_write( $name, $struct, 0 );
108 0 0         delete $struct->{xmlns} if $xmlns;
109              
110 0           $xml = join('', @$pi ) . $xml;
111 0 0         $xml = "\n$xml" if $param{header};
112              
113 0           return $xml;
114             }
115              
116              
117             sub struct {
118 0     0 1   my ($self, $json) = @_;
119 0           my $struct = { };
120 0           foreach my $property (keys %$self) {
121 0 0         next unless $property =~ /^[a-z]+$/;
122 0 0 0       if (ref $self->{$property} eq 'ARRAY') {
    0 0        
    0          
    0          
123 0           $struct->{$property} = [ map { $_->struct($json) } @{$self->{$property}} ];
  0            
  0            
124             } elsif ( UNIVERSAL::isa( $self->{$property}, "DAIA::Object" ) ) {
125 0           $struct->{$property} = $self->{$property}->struct;
126             } elsif ( UNIVERSAL::isa( $self->{$property}, 'JSON::Boolean' ) and not $json ) {
127 0 0         $struct->{$property} = $self->{$property} ? 'true' : 'false';
128             } elsif( $property eq 'label' and $self->{$property} eq '' ) {
129             # ignore empty string label
130             } else {
131             # remove characters not allowed in XML 1.0
132 0           my $value = $self->{$property};
133 0           $value =~ s/[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]//go;
134 0           $struct->{$property} = $value;
135             }
136             }
137 0           return $struct;
138             }
139              
140              
141             sub json {
142 0     0 1   my ($self, $callback) = @_;
143 0 0 0       $callback = $self->{_hidden}->{callback}
      0        
144             if @_ < 2 and $self->{_hidden} and exists $self->{_hidden}->{callback};
145 0           my $json = JSON->new->pretty->encode( $self->struct(1) );
146 0 0 0       if ( defined $callback and $callback =~ /^[a-z][a-z0-9._\[\]]*$/i ) {
147 0           return "$callback($json);"
148             } else {
149 0           return $json;
150             }
151             }
152              
153              
154             sub rdfhash {
155 0     0 1   return { };
156             }
157              
158              
159             sub serialize {
160 0     0 1   my ($self, $format) = @_;
161 0 0 0       return unless $format and grep { $_ eq $format } DAIA->formats;
  0            
162              
163 0           my $content = '';
164              
165 0 0         if ($format eq 'xml') {
    0          
    0          
    0          
166 0           $content = $self->xml(xmnls => 1);
167             } elsif ($format eq 'json') {
168 0           $content = $self->json;
169             } elsif ($format eq 'rdfjson') {
170 0           $content = JSON->new->pretty->encode($self->rdfhash());
171             } elsif ( $DAIA::TRINE_SERIALIZER ) {
172 0           my %opt;
173             # NOTE: RDF/XML dumps all namespaces, so avoid it
174 0 0 0       $opt{namespaces} = $DAIA::RDF_NS if $DAIA::RDF_NS and $format ne 'rdfxml';
175 0           my $ser;
176 0 0 0       if ( $DAIA::GRAPHVIZ and $DAIA::TRINE_MODEL and $format =~ /^(dot|svg)$/ ) {
      0        
177 0           $ser = $DAIA::GRAPHVIZ->new( as => $format, %opt );
178             } else {
179 0           $ser = eval { $DAIA::TRINE_SERIALIZER->new( $format, %opt ); };
  0            
180             }
181 0 0         if ($ser) {
182             # NOTE: We could get rid of RDF::Trine::Model if hashref converted directly to iterator
183 0           my $model = $DAIA::TRINE_MODEL->temporary_model;
184 0           $model->add_hashref( $self->rdfhash );
185 0           $content = $ser->serialize_model_to_string( $model );
186             }
187             }
188              
189 0           return $content;
190             }
191              
192              
193             sub rdfuri {
194 0     0 1   my $self = shift;
195 0 0         return $self->{id} if $self->{id};
196 0           my $id = lc(ref($self)).refaddr($self);
197 0           $id =~ s/.*::/_:/;
198 0           return $id;
199             }
200              
201 0     0 0   sub rdftype { }
202              
203              
204              
205             sub AUTOLOAD {
206 0     0     my $self = shift;
207 0 0         my $class = ref($self) or croak "$self is not an object";
208              
209 0           my $method = $AUTOLOAD;
210 0           $method =~ s/.*:://;
211 0 0         return if $method eq 'DESTROY';
212              
213 0           my $property = $method;
214 0 0         $property = lc($2) if $property =~ /^(add|provide)([A-Z][a-z]+)$/;
215              
216 1     1   5 no strict 'refs'; ##no critic
  1         2  
  1         1521  
217 0           my $PROPERTIES = \%{$class."::PROPERTIES"};
  0            
218              
219 0 0         croak "Method $class->$method ($property) does not exist"
220             unless exists $PROPERTIES->{$property};
221              
222 0           my $opt = $PROPERTIES->{$property};
223              
224             # TODO: conflicting properties?
225            
226 0 0         if ( $method =~ /^add/ ) {
    0          
227 0 0 0       croak "$class->$property is not repeatable or has no type"
228             unless $opt->{repeatable} and $opt->{type};
229 0           my $value = $_[0];
230 0 0         if ( not UNIVERSAL::isa( $_[0], $opt->{type} ) ) {
231 0           $value = eval $opt->{type}."->new( \@_ )"; ##no critic
232 0 0         croak $@ if $@;
233             }
234 0           return $self->add( $value );
235             } elsif( $method =~ /^provide/ ) { # set only if not set
236 0 0         if ( defined $self->{$property} ) {
237             # getter
238 0 0         return $opt->{repeatable} ? @{$self->{$property} || []} : $self->{$property}
  0 0          
239             } else {
240 0           return eval "\$self->$property(\@_)"; ##no critic
241             }
242             }
243              
244             # called as getter
245 0 0         return $opt->{repeatable} ? @{$self->{$property} || []} : $self->{$property}
  0 0          
    0          
246             if ( @_ == 0 );
247              
248 0           my $value = $_[0];
249              
250             # called as clearer (may imply setting the default value)
251 0 0 0       if (not defined $value or (ref($value) eq 'ARRAY' and @{$value} == 0)) {
  0   0        
252 0 0         if ( exists $opt->{default} ) {
253 0 0         $value = ref($opt->{default}) eq 'CODE'
254             ? $opt->{default}() : $opt->{default};
255             }
256 0 0         if ( defined $value ) {
257 0           $self->{$property} = $value;
258             } else {
259 0 0         delete $self->{$property} if exists $self->{$property};
260             }
261 0           return;
262             }
263              
264 0 0         if ( $opt->{type} ) {
265             # set one or more typed values
266              
267             # arguments must be either an array ref or a list of types or a simple list
268 0           my @args;
269              
270 0 0         if ( ref($_[0]) eq 'ARRAY' ) {
    0          
271 0 0         croak "too many arguments" if @_ > 1;
272 0           @args = @{$_[0]};
  0            
273             } elsif ( UNIVERSAL::isa( $_[0], $opt->{type} ) ) {
274             # treat ( $obj, ... ) as ( [ $obj, ... ] )
275 0           @args = @_;
276             } else {
277 0           @args = ( [ @_ ] ); # one element
278             }
279              
280 0 0 0       croak "$class->$property is not repeatable"
281             if ( @args > 1 and not $opt->{repeatable});
282              
283 0           my @values = map {
284 0           my $v;
285 0 0         if ( ref($_) eq 'ARRAY' ) {
    0          
286 0           $v = eval $opt->{type}.'->new( @{$_} )'; ##no critic
287 0 0         croak $@ if $@;
288             } elsif ( UNIVERSAL::isa( $_, $opt->{type} ) ) {
289 0           $v = $_;
290             } else {
291 0           $v = eval $opt->{type}.'->new( $_ )'; ##no critic
292 0 0         croak $@ if $@;
293             }
294 0           $v;
295             } @args;
296              
297 0 0         $self->{$property} = $opt->{repeatable} ? \@values : $values[0];
298              
299             } else {
300             # set an untyped value (never repeatable, stringified unless filtered)
301 0 0         if( $opt->{fixed} ) {
    0          
302 0           $value = $opt->{fixed};
303             } elsif( $opt->{filter} ) {
304 0           $value = $opt->{filter}( @_ );
305 0 0         croak "$class->$property did not pass value constraint: " . join(',',@_)
306             unless defined $value;
307             } else {
308 0           $value = "$value";
309             }
310              
311 0           $self->{$property} = $value;
312             }
313              
314 0           $self; # if called as setter, return the object for chaining
315             }
316              
317              
318             sub xml_write {
319 0     0 1   my ($name, $struct, $level) = @_;
320              
321 0           my $indent = (' ' x $level);
322 0           my $tag = "$indent<$name";
323              
324 0           my $content = '';
325 0 0         if (defined $struct->{content}) {
326 0           $content = $struct->{content};
327 0           delete $struct->{content};
328             }
329              
330 0 0         my @attr = grep { ! ref($struct->{$_}) and $_ ne 'label' } keys %$struct;
  0            
331 0           @attr = map { "$_=\"".xml_escape_value($struct->{$_}).'"' } @attr;
  0            
332 0 0         $tag .= " " . join(" ", @attr) if @attr;
333              
334             # get the right order
335 0           my @order = qw(message institution document label department storage available unavailable);
336 0           my @children = grep { defined $struct->{$_} } @order;
  0            
337 0           my %has = map { $_ => 1 } @children;
  0            
338             # append additional children
339 0 0         push @children, grep { ref($struct->{$_}) and not $has{$_} } keys %$struct;
  0            
340              
341 0           my @lines;
342 0 0         if (@children) {
343 0           push @lines, "$tag>";
344 0           foreach my $k (@children) {
345 0           $k =~ s/^\d//;
346 0 0         if ( $k eq 'label' ) {
    0          
    0          
347 0           push @lines, "$indent ";
348             } elsif ( ref($struct->{$k}) eq 'HASH' ) {
349 0           push @lines, xml_write($k, $struct->{$k}, $level+1);
350             } elsif ( ref($struct->{$k}) eq 'ARRAY' ) {
351 0           foreach my $v (@{$struct->{$k}}) {
  0            
352 0           push @lines, xml_write($k, $v, $level+1);
353             }
354             }
355             }
356 0           push @lines, "$indent";
357             } else {
358 0 0         if ( $content ne '' ) {
359 0           push @lines, "$tag>" . xml_escape_value($content) . "";
360             } else {
361 0           push @lines, "$tag />";
362             }
363             }
364            
365 0           return join("\n", @lines);
366             }
367              
368              
369             sub xml_escape_value {
370 0     0 1   my($data) = @_;
371 0 0         return '' unless defined($data);
372 0           $data =~ s/&/&/sg;
373 0           $data =~ s/
374 0           $data =~ s/>/>/sg;
375 0           $data =~ s/"/"/sg;
376 0           return $data;
377             }
378              
379              
380             sub _buildargs {
381 0     0     shift;
382 0 0         croak "uneven parameter list" if (@_ % 2);
383 0           @_;
384             };
385              
386              
387             sub _hidden_prop {
388 0     0     my $self = shift;
389 0 0         return unless $self->{_hidden};
390              
391 0           my $hashref = shift;
392 0           foreach ( @HIDDEN_PROPERTIES ) {
393 0 0 0       next if exists $hashref->{$_} or not exists $self->{_hidden}->{$_};
394 0           $hashref->{$_} = $self->{_hidden}->{$_};
395             }
396             }
397              
398              
399             sub _enable_utf8_layer {
400 0     0     my $fh = shift;
401 0 0         return unless eval{ can($fh, 'binmode'); };
  0            
402 0           foreach my $layer ( PerlIO::get_layers( $fh ) ) {
403 0 0         return if $layer =~ /^encoding|^utf8/;
404             }
405 0           binmode $fh, ':encoding(UTF-8)';
406             }
407              
408             our %COMMON_PROPERTIES =(
409             id => {
410             filter => sub { my $v = "$_[0]"; $v =~ s/^\s+|\s$//g; is_uri($v) ? $v : undef; }
411             },
412             href => {
413             filter => sub { my $v = "$_[0]"; is_web_uri($v) ? $v : undef; },
414             predicate => 'http://xmlns.com/foaf/0.1/page',
415             rdftype => 'resource'
416             },
417             message => {
418             type => 'DAIA::Message',
419             repeatable => 1,
420             predicate => 'http://purl.org/dc/terms/description',
421             },
422             );
423              
424             1;
425              
426             __END__