File Coverage

blib/lib/Catmandu/Importer/RDF.pm
Criterion Covered Total %
statement 113 129 87.6
branch 37 54 68.5
condition 10 21 47.6
subroutine 25 25 100.0
pod 0 3 0.0
total 185 232 79.7


line stmt bran cond sub pod time code
1             package Catmandu::Importer::RDF;
2              
3 4     4   339306 use open ':std', ':encoding(utf8)';
  4         2245  
  4         26  
4 4     4   21953 use namespace::clean;
  4         6  
  4         31  
5 4     4   493 use Catmandu::Sane;
  4         5  
  4         30  
6 4     4   970 use Moo;
  4         6  
  4         27  
7 4     4   5575 use RDF::Trine::Parser;
  4         2262456  
  4         122  
8 4     4   26 use RDF::Trine::Model;
  4         7  
  4         86  
9 4     4   17 use RDF::Trine::Store::SPARQL;
  4         6  
  4         105  
10 4     4   1901 use RDF::Trine::Store::LDF;
  4         2245603  
  4         150  
11 4     4   34 use RDF::Trine::Store;
  4         6  
  4         74  
12 4     4   25 use RDF::Query;
  4         7  
  4         78  
13 4     4   18 use RDF::LDF;
  4         7  
  4         58  
14 4     4   1951 use RDF::aREF;
  4         31917  
  4         267  
15 4     4   23 use RDF::aREF::Encoder;
  4         5  
  4         72  
16 4     4   52 use RDF::NS;
  4         6  
  4         67  
17 4     4   1781 use LWP::UserAgent::CHICaching;
  4         389486  
  4         7329  
18              
19             our $VERSION = '0.30';
20              
21             with 'Catmandu::RDF';
22             with 'Catmandu::Importer';
23              
24             has url => (
25             is => 'ro'
26             );
27              
28             has base => (
29             is => 'ro',
30             lazy => 1,
31             builder => sub {
32 8 50   8   875 defined $_[0]->file ? "file://".$_[0]->file : "http://example.org/";
33             }
34             );
35              
36             has encoder => (
37             is => 'ro',
38             lazy => 1,
39             builder => sub {
40 11     11   1479 my $ns = $_[0]->ns;
41 11 100 50     204 RDF::aREF::Encoder->new(
42             ns => (($ns // 1) ? $ns : { }),
43             subject_map => !$_[0]->predicate_map,
44             );
45             }
46             );
47              
48             has sparql => (
49             is => 'ro',
50             lazy => 1,
51             trigger => sub {
52             my ($sparql, $ns) = ($_[1], $_[0]->ns);
53             $sparql = do { local (@ARGV,$/) = $sparql; <> } if $sparql =~ /^\S+$/ && -r $sparql;
54             my %prefix;
55             # guess requires prefixes (don't override existing). Don't mind false positives
56             $prefix{$_} = 1 for ($sparql =~ /\s([a-z][a-z0-0_-]*):/mig);
57             delete $prefix{$_} for ($sparql =~ /PREFIX\s+([^:]+):/mg);
58             $_[0]->{sparql} = join "\n", (map { $ns->SPARQL($_) } keys %prefix), $sparql;
59             }
60             );
61              
62             has sparql_result => (
63             is => 'ro',
64             default => sub { 'simple' }
65             );
66              
67             has predicate_map => (
68             is => 'ro',
69             );
70              
71             has triples => (
72             is => 'ro',
73             );
74              
75             has cache => (
76             is => 'ro',
77             default => sub { 0 }
78             );
79              
80             has cache_options => (
81             is => 'ro',
82             default => sub { +{
83             driver => 'Memory',
84             global => 1 ,
85             max_size => 1024*1024
86             } }
87             );
88              
89             sub BUILD {
90 16     16 0 356 my ($self) = @_;
91              
92 16 50       328 if ($self->cache) {
93 0   0     0 my $options = $self->cache_options // {};
94 0         0 my $cache = CHI->new( %$options );
95 0         0 my $ua = LWP::UserAgent::CHICaching->new(cache => $cache);
96 0         0 RDF::Trine->default_useragent($ua);
97             }
98             }
99              
100             sub generator {
101             my ($self) = @_;
102              
103             if ($self->sparql) {
104             return $self->sparql_generator;
105             } else {
106             return $self->rdf_generator;
107             }
108             }
109              
110             sub sparql_generator {
111 3     3 0 7 my ($self) = @_;
112              
113 3 50       15 warn "--triples not active for sparql queries" if ($self->triples);
114 3 50       14 warn "--predicate_map not active for sparql queries" if ($self->predicate_map);
115              
116 3         40 my $encoder = RDF::aREF::Encoder->new( ns => {} ); # never return qnames
117              
118             sub {
119 3     3   33 state $stream = $self->_sparql_stream;
120 3 50 33     1744 if (defined($stream) && defined(my $row = $stream->next)) {
121 3 50 66     449140 if (ref $row eq 'RDF::Query::VariableBindings' || ref $row eq 'RDF::Trine::VariableBindings') {
122 3         11 my $ref = {};
123 3         42 for (keys %$row) {
124 4         20 my $val = $row->{$_};
125             $ref->{$_} = $self->sparql_result eq 'aref'
126 4 50       33 ? $encoder->object($val) : do { # TODO: clean up
127 4 100       39 if ( $val->is_resource ) {
    50          
128 2         32 $val->uri_value;
129             } elsif ( $val->is_literal) {
130 2         50 $val->literal_value;
131             } else {
132 0         0 $val->as_string
133             }
134             };
135             }
136 3         89 return $ref;
137             } else {
138 0         0 die "Expected a RDF::Query::VariableBindings or RDF::Trine::VariableBindings but got a " . ref($row);
139             }
140             } else {
141 0         0 return ($stream = undef);
142             }
143 3         192 };
144             }
145              
146             sub rdf_generator {
147 11     11 0 18 my ($self) = @_;
148             sub {
149 15     15   143 state $stream = $self->_rdf_stream;
150 15 50       16493 return unless $stream;
151              
152 15         97 my $aref = { };
153 15 100       75 if ($self->triples) {
154 5 100       8 if (my $triple = $stream->next) {
155 4         254 $aref = $self->encoder->triple(
156             $triple->subject,
157             $triple->predicate,
158             $triple->object
159             );
160             } else {
161 1         35 return ($stream = undef);
162             }
163             } else {
164             # TODO: include namespace mappings if requested
165 10         211 $self->encoder->add_hashref( $stream->as_hashref, $aref );
166              
167 10 100       60413 if ($self->url) {
168 3         10 $aref->{_url} = $self->url;
169             }
170              
171 10         25 $stream = undef;
172             }
173              
174 14 100       5973 if ($self->url) {
175             # RDF::Trine::Parser parses data from URL to UTF-8
176             # but we want internal character sequences
177 3         11 _utf8_decode($aref);
178             }
179              
180 14         79 return $aref;
181 11         100 };
182             }
183              
184             sub _utf8_decode {
185 9 50   9   18 if (ref $_[0] eq 'HASH') {
186             # FIXME: UTF-8 in property values
187 9         10 foreach (values %{$_[0]}) {
  9         18  
188 21 100       49 ref($_) ? _utf8_decode($_) : utf8::decode($_);
189             }
190             } else {
191 0         0 foreach (@{$_[0]}) {
  0         0  
192 0 0       0 ref($_) ? _utf8_decode($_) : utf8::decode($_);
193             }
194             }
195             }
196              
197             sub _sparql_stream {
198 3     3   6 my ($self) = @_;
199              
200 3 50       16 die "need an url" unless $self->url;
201              
202 3         67 $self->log->info("parsing: " . $self->sparql);
203              
204 3         642 my $store;
205              
206             # Check if this server is an LDF server
207 3         53 my $ldf_client = RDF::LDF->new(url => $self->url);
208              
209 3 100       1142 if ($ldf_client->is_fragment_server) {
210 2         387610 $store = RDF::Trine::Store->new_with_config({
211             storetype => 'LDF',
212             url => $self->url
213             });
214             }
215             else {
216 1         7017 $store = RDF::Trine::Store->new_with_config({
217             storetype => 'SPARQL',
218             url => $self->url
219             });
220             }
221              
222 3 50       381692 unless ($store) {
223 0         0 $self->log->error("failed to connect to " . $self->url);
224 0         0 return;
225             }
226              
227 3         27 my $model = RDF::Trine::Model->new($store);
228              
229 3         82 my $rdf_query = RDF::Query->new($self->sparql);
230              
231 3 50       12092 unless ($rdf_query) {
232 0         0 $self->log->error("failed to parse " . $self->sparql);
233 0         0 return;
234             }
235              
236 3         15 my $iterator = $rdf_query->execute($model);
237              
238 3 50       47031 unless ($iterator) {
239 0         0 $self->log->error("failed to execute " . $self->sparql . " at " . $self->url);
240 0         0 return;
241             }
242             }
243              
244             sub _rdf_stream {
245 11     11   16 my ($self) = @_;
246              
247 11         130 my $model = RDF::Trine::Model->new;
248 11 100       891 my $parser = $self->type
249             ? RDF::Trine::Parser->new( $self->type ) : 'RDF::Trine::Parser';
250              
251 11 100       26810 if ($self->url) {
252 3         28 $parser->parse_url_into_model( $self->url, $model );
253             } else {
254 8   50     48 my $from_scalar = (ref $self->file // '') eq 'SCALAR';
255 8 50 66     1235 if (!$self->type and $self->file and !$from_scalar) {
      66        
256 6         217 $parser = $parser->guess_parser_by_filename($self->file);
257             }
258 8 100       288 if ($from_scalar) {
259 2         32 $parser->parse_into_model( $self->base, ${$self->file}, $model );
  2         80  
260             } else {
261 6   33     83 $parser->parse_file_into_model( $self->base, $self->file // $self->fh, $model );
262             }
263             }
264            
265 11         223457 return $model->as_stream;
266             }
267              
268             1;
269             __END__
270              
271             =head1 NAME
272              
273             Catmandu::Importer::RDF - parse RDF data
274              
275             =head1 SYNOPSIS
276              
277             Command line client C<catmandu>:
278              
279             catmandu convert RDF --url http://d-nb.info/gnd/4151473-7 to YAML
280              
281             catmandu convert RDF --file rdfdump.ttl to JSON
282              
283             # Query a SPARQL endpoint
284             catmandu convert RDF --url http://dbpedia.org/sparql
285             --sparql "SELECT ?film WHERE { ?film dct:subject <http://dbpedia.org/resource/Category:French_films> }"
286              
287             catmandu convert RDF --url http://example.org/sparql --sparql query.rq
288              
289             # Query a Linked Data Fragment endpoint
290             catmandu convert RDF --url http://fragments.dbpedia.org/2014/en
291             --sparql "SELECT ?film WHERE { ?film dct:subject <http://dbpedia.org/resource/Category:French_films> }"
292              
293             In Perl code:
294              
295             use Catmandu::Importer::RDF;
296             my $url = "http://dx.doi.org/10.2474/trol.7.147";
297             my $rdf = Catmandu::Importer::RDF->new( url => $url )->first;
298              
299             =head1 DESCRIPTION
300              
301             This L<Catmandu::Importer> can be use to import RDF data from URLs, files or
302             input streams, SPARQL endpoints, and Linked Data Fragment endpoints.
303              
304             By default an RDF graph is imported as single item in aREF format (see
305             L<RDF::aREF>).
306              
307             =head1 CONFIGURATION
308              
309             =over
310              
311             =item url
312              
313             URL to retrieve RDF from.
314              
315             =item type
316              
317             RDF serialization type (e.g. C<ttl> for RDF/Turtle).
318              
319             =item base
320              
321             Base URL. By default derived from the URL or file name.
322              
323             =item ns
324              
325             Use default namespace prefixes as provided by L<RDF::NS> to abbreviate
326             predicate and datatype URIs. Set to C<0> to disable abbreviating URIs.
327             Set to a specific date to get stable namespace prefix mappings.
328              
329             =item triples
330              
331             Import each RDF triple as one aREF subject map (default) or predicate map
332             (option C<predicate_map>), if enabled.
333              
334             =item predicate_map
335              
336             Import RDF as aREF predicate map, if possible.
337              
338             =item file
339              
340             =item fh
341              
342             =item encoding
343              
344             =item fix
345              
346             Default configuration options of L<Catmandu::Importer>.
347              
348             =item sparql
349              
350             The SPARQL query to be executed on the URL endpoint (currectly only SELECT is
351             supported). The query can be supplied as string or as filename. The importer
352             tries to automatically add missing PREFIX statements from the default namespace
353             prefixes.
354              
355             =item sparql_result
356              
357             Encoding of SPARQL result values. With C<aref>, query results are encoded in
358             aREF format, with URIs in C<E<lt>> and C<E<gt>> (no qNames) and literal nodes
359             appended by C<@> and optional language code. By default (value C<simple>), all
360             RDF nodes are simplfied to their literal form.
361              
362             =item cache
363              
364             Set to a true value to cache repeated URL responses in a L<CHI> based backend.
365              
366             =item cache_options
367              
368             Provide the L<CHI> based options for caching result sets. By default a memory store of
369             1MB size is used. This is equal to:
370              
371             Catamandu::Importer::RDF->new( ...,
372             cache => 1,
373             cache_options => {
374             driver => 'Memory',
375             global => 1,
376             max_size => 1024*1024
377             });
378              
379             =back
380              
381             =head1 METHODS
382              
383             See L<Catmandu::Importer>.
384              
385             =head1 SEE ALSO
386              
387             L<RDF::Trine::Store>, L<RDF::Trine::Parser>
388              
389             =encoding utf8
390              
391             =cut