File Coverage

blib/lib/DAIA.pm
Criterion Covered Total %
statement 32 40 80.0
branch 3 6 50.0
condition n/a
subroutine 11 11 100.0
pod n/a
total 46 57 80.7


line stmt bran cond sub pod time code
1 11     11   282725 use strict;
  11         25  
  11         422  
2 11     11   56 use warnings;
  11         23  
  11         1755  
3             package DAIA;
4             #ABSTRACT: Document Availability Information API
5             our $VERSION = '0.43'; #VERSION
6              
7             # we do not want depend on the following modules
8             our ($TRINE_MODEL, $TRINE_SERIALIZER, $RDF_NS, $GRAPHVIZ);
9             BEGIN {
10             # optionally use RDF::Trine::Serializer
11 11     11   25 $TRINE_MODEL = 'RDF::Trine::Model';
12 11         73 $TRINE_SERIALIZER = 'RDF::Trine::Serializer';
13 11     11   786 eval "use $TRINE_MODEL; use $TRINE_SERIALIZER";
  11         4888  
  0         0  
  0         0  
14 11 50       64 if ($@) {
15 11         21 $TRINE_MODEL = undef;
16 11         21 $TRINE_SERIALIZER = undef;
17             }
18             # optionally use RDF::NS
19 11     11   672 eval "use RDF::NS";
  11         4407  
  0         0  
  0         0  
20 11 50       57 $RDF_NS = eval "RDF::NS->new('any')" unless $@;
21             # optionally use RDF::Trine::Exporter::GraphViz
22 11     11   625 eval "use RDF::Trine::Exporter::GraphViz";
  11         4217  
  0         0  
  0         0  
23 11 50       360 $GRAPHVIZ = 'RDF::Trine::Exporter::GraphViz' unless $@;
24             }
25              
26 11     11   57 use base 'Exporter';
  11         19  
  11         2245  
27             our %EXPORT_TAGS = (
28             core => [qw(response document item available unavailable availability)],
29             entities => [qw(institution department storage limitation)],
30             );
31             our @EXPORT_OK = qw(is_uri parse guess);
32             Exporter::export_ok_tags;
33             $EXPORT_TAGS{all} = [@EXPORT_OK, 'message'];
34             Exporter::export_tags('all');
35              
36 11     11   59 use Carp; # use Carp::Clan; # qw(^DAIA::);
  11         22  
  11         1188  
37 11     11   11060 use IO::File;
  11         128207  
  11         3466  
38 11     11   11798 use LWP::Simple ();
  11         1238484  
  11         388  
39 11     11   20888 use XML::LibXML::Simple qw(XMLin);
  0            
  0            
40              
41             use DAIA::Response;
42             use DAIA::Document;
43             use DAIA::Item;
44             use DAIA::Availability;
45             use DAIA::Available;
46             use DAIA::Unavailable;
47             use DAIA::Message;
48             use DAIA::Entity;
49             use DAIA::Institution;
50             use DAIA::Department;
51             use DAIA::Storage;
52             use DAIA::Limitation;
53              
54             use Data::Validate::URI qw(is_uri);
55              
56              
57             sub response { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Response->new( @_ ) }
58             sub document { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Document->new( @_ ) }
59             sub item { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Item->new( @_ ) }
60             sub available { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Available->new( @_ ) }
61             sub unavailable { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Unavailable->new( @_ ) }
62             sub availability { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Availability->new( @_ ) }
63             sub message { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Message->new( @_ ) }
64             sub institution { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Institution->new( @_ ) }
65             sub department { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Department->new( @_ ) }
66             sub storage { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Storage->new( @_ ) }
67             sub limitation { local $Carp::CarpLevel = $Carp::CarpLevel + 1; return DAIA::Limitation->new( @_ ) }
68              
69              
70             sub parse {
71             shift if UNIVERSAL::isa( $_[0], __PACKAGE__ );
72             my ($from, %param) = (@_ % 2) ? (@_) : (undef,@_);
73             $from = $param{from} unless defined $from;
74             $from = $param{data} unless defined $from;
75             my $format = lc( $param{format} || '' );
76             my $file = $param{file};
77             $file = $from if defined $from and $from =~ /^http(s)?:\/\//;
78             if (not defined $file and defined $from and not defined $param{data}) {
79             if( ref($from) eq 'GLOB' or UNIVERSAL::isa($from, 'IO::Handle')) {
80             $file = $from;
81             } elsif( $from eq '-' ) {
82             $file = \*STDIN;
83             } elsif( $from =~ /\.(xml|json)$/ ) {
84             $file = $from ;
85             $format = $1 unless $format;
86             }
87             }
88             if ( $file ) {
89             if ( $file =~ /^http(s)?:\/\// ) {
90             $from = LWP::Simple::get($file) or croak "Failed to fetch $file via HTTP";
91             } else {
92             if ( ! (ref($file) eq 'GLOB' or UNIVERSAL::isa( $file, 'IO::Handle') ) ) {
93             $file = do { IO::File->new($file, '<:encoding(UTF-8)') or croak("Failed to open file $file") };
94             }
95             # Enable :encoding(UTF-8) layer unless it or some other encoding has already been enabled
96             # foreach my $layer ( PerlIO::get_layers( $file ) ) {
97             # return if $layer =~ /^encoding|^utf8/;
98             #}
99             binmode $file, ':encoding(UTF-8)';
100             $from = do { local $/; <$file> };
101             }
102             croak "DAIA serialization is empty" unless $from;
103             }
104              
105             croak "Missing source to parse from " unless defined $from;
106              
107             $format = guess($from) unless $format;
108              
109             my $value;
110             my @objects;
111             my $root = 'Response';
112              
113             if ( $format eq 'xml' ) {
114             # do not look for filename (security!)
115             if (defined $param{data} and guess($from) ne 'xml') {
116             croak("XML is not well-formed (<...>)");
117             }
118              
119             if (guess($from) eq 'xml') {
120             utf8::encode($from);;
121             #print "IS UTF8?". utf8::is_utf8($from) . "\n";
122             }
123              
124             my $xml = _parse_daia_xml($from);
125              
126             croak $@ if $@;
127             croak "XML does not contain DAIA elements" unless $xml;
128              
129             while (my ($root,$value) = each(%$xml)) {
130             $root =~ s/{[^}]+}//;
131             $root = ucfirst($root);
132             $root = 'Response' if $root eq 'Daia';
133              
134             _filter_xml( $value ); # filter out all non DAIA elements and namespaces
135              
136             $value = [ $value ] unless ref($value) eq 'ARRAY';
137              
138             foreach my $v (@$value) {
139             # TODO: croak of $root is not known!
140             my $object = eval 'DAIA::'.$root.'->new( $v )'; ##no critic
141             croak $@ if $@;
142             push @objects, $object;
143             }
144             }
145              
146             } elsif ( $format eq 'json' ) {
147             eval { $value = JSON->new->decode($from); };
148             croak $@ if $@;
149              
150             if ( (keys %$value) == 1 ) {
151             my ($k => $v) = %$value;
152             if (not $k =~ /^(timestamp|message|institution|document)$/ and ref($v) eq 'HASH') {
153             ($root, $value) = (ucfirst($k), $v);
154             }
155             }
156              
157             # outdated variants
158             $root = "Response" if $root eq 'Daia';
159             delete $value->{'xmlns:xsi'};
160              
161             delete $value->{schema} if $root eq 'Response'; # ignore schema attribute
162              
163             croak "JSON does not contain DAIA elements" unless $value;
164             push @objects, eval('DAIA::'.$root.'->new( $value )'); ##no critic
165             croak $@ if $@;
166              
167             } else {
168             croak "Unknown DAIA serialization format $format";
169             }
170              
171             return if not wantarray and @objects > 1;
172             return wantarray ? @objects : $objects[0];
173             }
174              
175              
176             sub parse_xml {
177             shift if UNIVERSAL::isa( $_[0], __PACKAGE__ );
178             DAIA::parse( shift, format => 'xml', @_ );
179             }
180              
181              
182             sub parse_json {
183             shift if UNIVERSAL::isa( $_[0], __PACKAGE__ );
184             DAIA::parse( shift, format => 'json' );
185             }
186              
187              
188             sub guess {
189             shift if UNIVERSAL::isa( $_[0], __PACKAGE__ );
190             my $data = shift;
191             return '' unless $data;
192             return 'xml' if $data =~ m{^\s*\<.*?\>\s*$}s;
193             return 'json' if $data =~ m{^\s*\{.*?\}\s*$}s;
194             return '';
195             }
196              
197              
198             sub formats {
199             shift if UNIVERSAL::isa( $_[0], __PACKAGE__ );
200             my %formats = (
201             xml => 'application/xml; charset=utf-8',
202             json => 'application/javascript; charset=utf-8',
203             rdfjson => 'application/javascript; charset=utf-8',
204             );
205              
206             if ($TRINE_SERIALIZER) {
207             $formats{'rdfxml'} = 'application/rdf+xml; charset=utf-8',;
208             $formats{'turtle'} = 'text/turtle; charset=utf-8';
209             $formats{'ntriples'} = 'text/plain';
210             }
211             if ($GRAPHVIZ) {
212             $formats{'svg'} = 'image/svg+xml';
213             $formats{'dot'} = 'text/plain; charset=utf-8';
214             }
215              
216             return %formats;
217             }
218              
219              
220             #### internal methods (subject to be changed)
221              
222             my $NSEXPDAIA = qr/{http:\/\/(ws.gbv.de|purl.org\/ontology)\/daia\/}(.*)/;
223              
224             sub _parse_daia_xml {
225             my ($from) = @_;
226             my $xml = eval { XMLin( $from, KeepRoot => 1, NSExpand => 1, KeyAttr => [ ], NormalizeSpace => 2 ); };
227             daia_xml_roots($xml);
228             }
229              
230             sub daia_xml_roots {
231             my $xml = shift; # hash reference
232             my $out = { };
233              
234             return { } unless UNIVERSAL::isa($xml,'HASH');
235              
236             foreach my $key (keys %$xml) {
237             my $value = $xml->{$key};
238              
239             if ( $key =~ /^{([^}]*)}(.*)/ and !($key =~ $NSEXPDAIA) ) {
240             # non DAIA element
241             my $children = UNIVERSAL::isa($value,'ARRAY') ? $value : [ $value ];
242             @$children = grep {defined $_} map { daia_xml_roots($_) } @$children;
243             foreach my $n (@$children) {
244             while ( my ($k,$v) = each(%{$n}) ) {
245             next if $k =~ /^xmlns/;
246             $v = [$v] unless UNIVERSAL::isa($v,'ARRAY');
247             if ($out->{$k}) {
248             push @$v, (UNIVERSAL::isa($out->{$k},'ARRAY') ?
249             @{$out->{$k}} : $out->{$k});
250             }
251             # filter out scalars
252             @$v = grep {ref($_)} @$v unless $k =~ $NSEXPDAIA;
253             if (@$v) {
254             $out->{$k} = (@$v > 1 ? $v : $v->[0]);
255             }
256             }
257             }
258             } else { # DAIA element or element without namespace
259             $out->{$key} = $value;
260             }
261             }
262              
263             return $out;
264             }
265              
266             # filter out non DAIA XML elements, 'xmlns' attributes and empty values
267             sub _filter_xml {
268             my $xml = shift;
269             map { _filter_xml($_) } @$xml if ref($xml) eq 'ARRAY';
270             return unless ref($xml) eq 'HASH';
271              
272             my (@del,%add);
273             foreach my $key (keys %$xml) {
274             my $value = $xml->{$key};
275             if ($key =~ /^{([^}]*)}(.*)/) {
276             my $local = $2;
277             if ($1 =~ /^http:\/\/(ws.gbv.de|purl.org\/ontology)\/daia\/$/ and $value ne '') {
278             $xml->{$local} = $xml->{$key};
279             }
280             push @del, $key;
281             } elsif ($key =~ /^xmlns/ or $key =~ /:/ or $value eq '') {
282             push @del, $key;
283             }
284             }
285              
286             # remove non-daia elements
287             foreach (@del) { delete $xml->{$_}; }
288              
289             # recurse
290             map { _filter_xml($xml->{$_}) } keys %$xml;
291             }
292              
293             1;
294              
295             __END__