File Coverage

blib/lib/Catmandu/Importer/SRU.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Catmandu::Importer::SRU;
2              
3 8     8   410650 use Catmandu::Sane;
  8         1045524  
  8         61  
4 8     8   4394 use Catmandu::Importer::SRU::Parser;
  0            
  0            
5             use Catmandu::Util qw(:is);
6             use URI::Escape;
7             use Moo;
8             use Furl;
9             use Carp;
10             use XML::LibXML;
11             use XML::LibXML::XPathContext;
12              
13             our $VERSION = '0.040';
14              
15             with 'Catmandu::Importer';
16              
17             # required.
18             has base => (is => 'ro', required => 1);
19             has query => (is => 'ro', required => 1);
20             has version => (is => 'ro', default => sub { '1.1' });
21             has operation => (is => 'ro', default => sub { 'searchRetrieve' });
22             has recordSchema => (is => 'ro', default => sub { 'dc' });
23             has userAgent => (is => 'ro', default => sub { 'Mozilla/5.0' });
24             has furl => (is => 'ro', lazy => 1, builder => sub {
25             Furl->new( agent => $_[0]->userAgent );
26             });
27              
28             # optional.
29             has sortKeys => (is => 'ro');
30             has parser => (is => 'rw', default => sub { 'simple' }, coerce => \&_coerce_parser );
31              
32             # internal stuff.
33             has _currentRecordSet => (is => 'ro');
34             has _n => (is => 'ro', default => sub { 0 });
35             has _start => (is => 'ro', default => sub { 1 });
36             has _max_results => (is => 'ro', default => sub { 10 });
37             has _meta_get => (is => 'ro');
38             has _meta_destr => (is => 'ro', default => sub { 1 });
39              
40             # Internal Methods. ------------------------------------------------------------
41              
42             sub _coerce_parser {
43             my ($parser) = @_;
44              
45             return $parser if is_invocant($parser) or is_code_ref($parser);
46              
47             if (is_string($parser) && !is_number($parser)) {
48             my $class = $parser =~ /^\+(.+)/ ? $1
49             : "Catmandu::Importer::SRU::Parser::$parser";
50              
51             my $parser;
52             eval {
53             $parser = Catmandu::Util::require_package($class)->new;
54             };
55             if ($@) {
56             croak $@;
57             } else {
58             return $parser;
59             }
60             }
61              
62             return Catmandu::Importer::SRU::Parser->new;
63             }
64              
65             # Internal: HTTP GET something.
66             #
67             # $url - the url.
68             #
69             # Returns the raw response object.
70             sub _request {
71             my ($self, $url) = @_;
72              
73             my $res = $self->furl->get($url);
74             die $res->status_line unless $res->is_success;
75              
76             return $res;
77             }
78              
79             # Internal: Converts XML to a perl hash.
80             #
81             # $in - the raw XML input.
82             #
83             # Returns a hash representation of the given XML.
84             sub _hashify {
85             my ($self, $in) = @_;
86              
87             my $parser = XML::LibXML->new();
88             my $doc = $parser->parse_string($in);
89             my $root = $doc->documentElement;
90             my @namespaces = $root->getNamespaces;
91              
92             my $xc = XML::LibXML::XPathContext->new( $root );
93             $xc->registerNs("srw","http://www.loc.gov/zing/srw/");
94             $xc->registerNs("d","http://www.loc.gov/zing/srw/diagnostic/");
95            
96             my $diagnostics = {};
97             my $meta;
98             my $records = {};
99              
100             if ($xc->exists('/srw:searchRetrieveResponse/srw:diagnostics')) {
101             $diagnostics->{diagnostic} = [];
102              
103             for ($xc->findnodes('/srw:searchRetrieveResponse/srw:diagnostics/*')) {
104             my $uri = $xc->findvalue('./d:uri',$_);
105             my $message = $xc->findvalue('./d:message',$_);
106             my $details = $xc->findvalue('./d:details',$_);
107              
108             push @{$diagnostics->{diagnostic}} ,
109             { uri => $uri , message => $message , details => $details } ;
110             }
111             } elsif ($self->_meta_get) {
112             for ($xc->findnodes('/srw:searchRetrieveResponse')) {
113             for ($xc->findnodes('./*', $_)) {
114             my $tagName = $_->tagName;
115             next if $tagName eq 'records';
116             if($tagName eq 'echoedSearchRetrieveRequest' or $tagName eq 'extraResponseData') {
117             my $key = $tagName;
118             $meta->{$key} = {};
119             for ($xc->findnodes("/srw:searchRetrieveResponse/srw:$key")) {
120             for ($xc->findnodes('./*', $_)) {
121             if(defined $_->prefix) {
122             $xc->registerNs($_->prefix,$_->namespaceURI());
123             }
124             my $tagName = $_->tagName;
125             $meta->{$key}->{$tagName} = $xc->findvalue(".",$_);
126             }
127             }
128             } else {
129             $meta->{$tagName} = $xc->findvalue(".",$_);
130             }
131             }
132             }
133             }
134            
135             if ($xc->exists('/srw:searchRetrieveResponse/srw:records')) {
136             $records->{record} = [];
137              
138             for ($xc->findnodes('/srw:searchRetrieveResponse/srw:records/srw:record')) {
139             my $recordSchema = $xc->findvalue('./srw:recordSchema',$_);
140             my $recordPacking = $xc->findvalue('./srw:recordPacking',$_);
141             my $recordData = $xc->find('./srw:recordData/*',$_)->pop();
142             my $recordPosition = $xc->findvalue('./srw:recordPosition',$_);
143              
144             # Copy all the root level namespaces to the record Element.
145             for (@namespaces) {
146             my $ns_prefix = $_->declaredPrefix;
147             my $ns_uri = $_->declaredURI;
148             # Skip the SRW namespaces
149             unless ($ns_uri =~ m{http://www.loc.gov/zing/srw/}) {
150             $recordData->setNamespace($ns_uri,$ns_prefix,0);
151             }
152             }
153              
154             push @{$records->{record}} ,
155             { recordSchema => $recordSchema , recordPacking => $recordPacking ,
156             recordData => $recordData , recordPosition => $recordPosition };
157             }
158             }
159              
160             return { diagnostics => $diagnostics , records => $records, meta => $meta };
161             }
162              
163             sub url {
164             my ($self) = @_;
165              
166             # construct the url
167             my $url = $self->base;
168             $url .= '?version=' . uri_escape($self->version);
169             $url .= '&operation=' .uri_escape($self->operation);
170             $url .= '&query=' . uri_escape($self->query);
171             $url .= '&recordSchema=' . uri_escape($self->recordSchema);
172             $url .= '&sortKeys=' . uri_esacpe($self->sortKeys) if $self->sortKeys;
173             $url .= '&startRecord=' . uri_escape($self->_start);
174             $url .= '&maximumRecords=' . uri_escape($self->_max_results);
175              
176             return $url;
177             }
178              
179             # Internal: gets the next set of results.
180             #
181             # Returns a array representation of the resultset.
182             sub _nextRecordSet {
183             my ($self) = @_;
184              
185             # fetch the xml response and hashify it.
186             my $res = $self->_request($self->url);
187             my $xml = $res->{content};
188             my $hash = $self->_hashify($xml);
189              
190             # sru specific error checking.
191             if (exists $hash->{'diagnostics'}->{'diagnostic'}) {
192             for my $error (@{$hash->{'diagnostics'}->{'diagnostic'}}) {
193             warn 'SRU DIAGNOSTIC: ', $error->{'message'} , ' : ' , $error->{'details'};
194             }
195             }
196              
197             # get to the point.
198             my $meta = $hash->{'meta'};
199             my $set = $hash->{'records'}->{'record'};
200              
201             # return a reference to a array.
202             return { record => \@{$set}, meta => $meta };
203             }
204              
205             # Internal: gets the next record from our current resultset.
206             #
207             # Returns a hash representation of the next record.
208             sub _nextRecord {
209             my ($self) = @_;
210              
211             # fetch recordset if we don't have one yet.
212             $self->{_currentRecordSet} = $self->_nextRecordSet unless $self->_currentRecordSet;
213              
214             # check for a exhaused recordset.
215             if ($self->_n >= $self->_max_results) {
216             $self->{_start} += $self->_max_results;
217             $self->{_n} = 0;
218             $self->{_currentRecordSet} = $self->_nextRecordSet;
219             }
220              
221             # return the next record or metadata.
222             my $record = $self->{_currentRecordSet}->{record}->[$self->{_n}++];
223              
224             if (defined $record) {
225             if (is_code_ref($self->parser)) {
226             $record = $self->parser->($record);
227             } else {
228             $record = $self->parser->parse($record);
229             }
230             }
231             return $record;
232             }
233              
234             # Internal: gets searchRetrieveResponse metadata of the request
235             #
236             # Returns a hash representation of the metadata.
237             sub _meta {
238             my ($self) = @_;
239              
240             my $meta;
241             if($self->_meta_destr) {
242             $self->{_currentRecordSet} = $self->_nextRecordSet;
243             $meta = $self->{_currentRecordSet}->{meta};
244             $meta = $self->parser->parse($meta);
245             $self->{_meta_destr} = 0;
246             }
247              
248             return $meta;
249             }
250              
251             # Public Methods. --------------------------------------------------------------
252              
253             sub generator {
254             my ($self) = @_;
255              
256             if (ref $self->parser eq 'Catmandu::Importer::SRU::Parser::meta') {
257             $self->{_meta_get} = 1;
258             return sub {
259             $self->_meta;
260             };
261             }
262              
263             return sub {
264             $self->_nextRecord;
265             };
266             }
267              
268             =head1 NAME
269              
270             Catmandu::Importer::SRU - Package that imports SRU data
271              
272             =head1 SYNOPSIS
273              
274             use Catmandu::Importer::SRU;
275              
276             my %attrs = (
277             base => 'http://www.unicat.be/sru',
278             query => '(isbn=0855275103 or isbn=3110035170 or isbn=9010017362 or isbn=9014026188)',
279             recordSchema => 'marcxml',
280             parser => 'marcxml'
281             );
282              
283             my $importer = Catmandu::Importer::SRU->new(%attrs);
284              
285             my $count = $importer->each(sub {
286             my $schema = $record->{recordSchema};
287             my $packing = $record->{recordPacking};
288             my $position = $record->{recordPosition};
289             my $data = $record->{recordData};
290             # ...
291             });
292              
293             # Using Catmandu::Importer::SRU::Package::marcxml, included in this release
294              
295             my $importer = Catmandu::Importer::SRU->new(
296             base => 'http://www.unicat.be/sru',
297             query => '(isbn=0855275103 or isbn=3110035170 or isbn=9010017362 or isbn=9014026188)',
298             recordSchema => 'marcxml' ,
299             parser => 'marcxml' ,
300             );
301              
302             # Using a homemade parser
303              
304             my $importer = Catmandu::Importer::SRU->new(
305             base => 'http://www.unicat.be/sru',
306             query => '(isbn=0855275103 or isbn=3110035170 or isbn=9010017362 or isbn=9014026188)',
307             recordSchema => 'marcxml' ,
308             parser => MyParser->new , # or parser => '+MyParser'
309             );
310              
311             =head1 CONFIGURATION
312              
313             =over
314              
315             =item base
316              
317             base URL of the SRU server (required)
318              
319             =item query
320              
321             CQL query (required)
322              
323             =item recordSchema
324              
325             set to C<dc> by default
326              
327             =item sortkeys
328              
329             optional sorting
330              
331             =item operation
332              
333             set to C<searchRetrieve> by default
334              
335             =item version
336              
337             set to C<1.1> by default.
338              
339             =item userAgent
340              
341             HTTP user agent, set to C<Mozilla/5.0> by default.
342              
343             =item furl
344              
345             Instance of L<Furl> or compatible class to fetch URLs with.
346              
347             =item parser
348              
349             Controls how records are parsed before importing. The following options
350             are possible:
351              
352             =over
353              
354             =item
355              
356             Instance of a Perl package that implements a C<parse> subroutine. See the
357             default value C<Catmandu::Importer::SRU::Parser> for an example.
358              
359             =item
360              
361             Name of a Perl package that implements a C<parse> subroutine. The name must be
362             prepended by C<+> or it prefixed with C<Catmandu::Importer::SRU::Parser::>. For
363             instance C<marcxml> will create a C<Catmandu::Importer::SRU::Parser::marcxml>.
364              
365             =item
366              
367             Function reference that gets passed the unparsed record.
368              
369             =back
370              
371             =back
372              
373             =head1 METHODS
374              
375             All methods of L<Catmandu::Importer> and by this L<Catmandu::Iterable> are
376             inherited. In addition the following methods are provided:
377              
378             =head2 url
379              
380             Return the current SRU request URL (useful for debugging).
381              
382             =head1 SEE ALSO
383              
384             L<Catmandu::Importer>,
385             L<Catmandu::Iterable>,
386             L<http://www.loc.gov/standards/sru/>
387              
388             =cut
389              
390             1;