File Coverage

blib/lib/PICA/Source.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package PICA::Source;
2             {
3             $PICA::Source::VERSION = '0.585';
4             }
5             #ABSTRACT: Data source that can be queried for PICA+ records
6 1     1   35259 use strict;
  1         3  
  1         47  
7              
8              
9 1     1   7 use Carp qw(croak);
  1         3  
  1         62  
10 1     1   645 use PICA::PlainParser;
  1         5  
  1         33  
11 1     1   628 use PICA::SRUSearchParser;
  1         3  
  1         25  
12 1     1   463 use PICA::Store;
  0            
  0            
13             use LWP::Simple;
14             use Unicode::Normalize qw(NFC);
15              
16              
17             sub new {
18             my ($class, %params) = @_;
19             $class = ref $class || $class;
20              
21             PICA::Store::readconfigfile( \%params, $ENV{PICASOURCE} )
22             if exists $params{config} or exists $params{conf} ;
23              
24             my $self = {
25             SRU => $params{SRU} ? $params{SRU} : undef,
26             Z3950 => $params{Z3950} ? $params{Z3950} : undef,
27             unAPI => $params{unAPI} ? $params{unAPI} : undef,
28             PSI => $params{PSI} ? $params{PSI} : undef,
29             user => $params{user} ? $params{user} : undef,
30             password => $params{password} ? $params{password} : undef,
31             idprefix => ($params{idprefix} || undef),
32             prev_record => undef,
33             Limit => ($params{Limit} || 10),
34             };
35              
36             if ($self->{SRU} and not $self->{SRU} =~ /[\?&]$/) {
37             $self->{SRU} .= ($self->{SRU} =~ /\?/) ? '&' : '?';
38             }
39             if ($self->{PSI}) {
40             $self->{PSI} =~ s/\/$//;
41             }
42              
43             bless $self, $class;
44             }
45              
46              
47             sub getPPN {
48             my ($self, $id) = @_;
49              
50             croak("No SRU, PSI or unAPI interface defined")
51             unless $self->{SRU} or $self->{unAPI} or $self->{PSI};
52              
53             if ( $self->{PSI} or $self->{unAPI} ) {
54             my $url;
55              
56             if ( $self->{PSI} ) {
57             $url = $self->{PSI} . "/PLAIN=ON/CHARSET=UTF8/PLAINTTLCHARSET=UTF8/URLENCODE=Y/PPN?PPN=$id";
58             } else {
59             $url = $self->{unAPI}
60             . ((index($self->{unAPI},'?') == -1) ? '?' : '&')
61             . "format=pp&id=";
62             if ( !($id =~ /ppn:/) and $self->{idprefix} ) {
63             $url .= $self->{idprefix} . ":ppn:$id";
64             } else {
65             $url .= $id;
66             }
67             # TODO: unapi server does not set encoding header (utf8)?
68             }
69              
70             my $data = LWP::Simple::get( $url );
71             if (not $data) {
72             $@ = "HTTP request failed: $url";
73             return;
74             }
75              
76             if ( $self->{PSI} ) {
77             utf8::downgrade( $data ); # make sure that the UTF-8 flag is off
78             $data = url_decode($data);
79             utf8::decode($data);
80             $data = NFC($data); # compose combining chars
81             utf8::upgrade($data);
82             }
83             my $record = eval { PICA::Record->new( $data ) } ;
84             if ($record) {
85             return $record;
86             } else {
87             $@ = "Failed to parse PICA::Record";
88             return;
89             }
90             } else {
91             my $result = $self->cqlQuery( "pica.ppn=$id", Limit => 1 );
92             my ($record) = $result->records();
93             return $record;
94             } # TODO: use z3950
95             }
96              
97              
98             sub cqlQuery {
99             my $self = shift;
100             my $cql = shift;
101              
102             croak("No SRU interface defined") unless $self->{SRU};
103              
104             my $xmlparser = UNIVERSAL::isa( $_[0], "PICA::XMLParser" )
105             ? $_[0] : PICA::XMLParser->new( @_ );
106             my $sruparser = PICA::SRUSearchParser->new( $xmlparser );
107             shift if ref($_[0]);
108             my %params = (@_);
109             my $limit = $params{Limit} || $self->{Limit};
110              
111             my $options = "";
112             $cql = url_encode($cql); #url_unicode_encode($cql);
113             my $baseurl = $self->{SRU} . "recordSchema=picaxml&version=1.1&operation=searchRetrieve&maximumRecords=$limit";
114              
115             my $startRecord = 1;
116             if ($xmlparser->{offset} > 0) {
117             $startRecord += $xmlparser->{offset};
118             $xmlparser->{offset} = 0;
119             }
120             while(1) {
121             my $options = "&startRecord=$startRecord";
122             my $url = $baseurl . "&query=" . $cql . $options;
123              
124             print "$url\n"; # TODO: logging
125              
126             my $xml = LWP::Simple::get( $url );
127             croak("SRU Request failed $url") unless $xml; # TODO: don't croak?
128             $xmlparser = $sruparser->parse($xml);
129              
130             #print "numberOfRecords " . $sruparser->numberOfRecords() . "\n";
131             #print "resultSetId " . $sruparser->resultSetId() . "\n";
132             #print "current counter " . $xmlparser->counter() . "\n";
133              
134             return $xmlparser unless $sruparser->currentNumber(); # zero results
135             $startRecord += $sruparser->currentNumber();
136             return $xmlparser if $sruparser->numberOfRecords() < $startRecord;
137             return $xmlparser if $xmlparser->finished();
138             }
139             }
140              
141              
142             sub z3950Query {
143             my ($self, $query, %handlers) = @_;
144              
145             eval { require ZOOM; require ZOOM::Options; require ZOOM::Connection; };
146             croak("Please load package ZOOM to use Z39.50!")
147             unless defined $INC{'ZOOM.pm'};
148             croak("No Z3950 interface defined") unless $self->{Z3950};
149             croak("Z3950 interface have host and database")
150             unless $self->{Z3950} =~ /^(tcp:|ssl:)?([^\/:]+)(:[0-9]+)?\/(.*)/;
151              
152             my $options = new ZOOM::Options();
153             $options->option( preferredRecordSyntax => "picamarc" );
154             $options->option( user => $self->{user} ) if defined $self->{user};
155             $options->option( password => $self->{password} ) if defined $self->{password};
156              
157             my ($conn, $rs);
158             eval {
159             $conn = ZOOM::Connection->create( $options );
160             $conn->connect( $self->{Z3950} );
161             };
162             eval { $rs = $conn->search_pqf($query); } unless $@;
163             if ($@) {
164             croak("Z39.50 error " . $@->code(), ": ", $@->message());
165             }
166              
167             %handlers = () unless %handlers;
168             $handlers{Proceed} = 1;
169              
170             my $parser = PICA::PlainParser->new( %handlers );
171             my $n = $rs->size();
172             for my $i (0..$n-1) {
173             my $raw;
174             eval {
175             $raw = $rs->record($i)->raw();
176             };
177             if ($@) {
178             croak("Z39.50 error " . $@->code(), ": ", $@->message());
179             }
180             #print "$raw\n";
181             $parser->parsedata($raw);
182             return $parser if $parser->finished();
183             }
184             return $parser;
185             }
186              
187              
188             sub iktQuery {
189             my ($self, $ikt, $term) = @_;
190              
191             croak('No PSI interface defined') unless $self->{PSI};
192              
193             $ikt = url_encode($ikt);
194             $term = url_encode($term);
195             # $term =~ s/\//\\\//; # escape / => \/
196              
197             my $url = $self->{PSI}
198             . "/PLAIN=ON/CHARSET=UTF8/PLAINTTLCHARSET=UTF8/"
199             . "CMD?ACT=SRCHA&IKT=$ikt&TRM=$term";
200             my $raw = get($url);
201             utf8::decode($raw);
202             $raw = NFC($raw); # compose combining chars
203             utf8::upgrade($raw);
204             my $record = eval { PICA::Record->new( $raw ); };
205            
206             return ($record);
207             }
208              
209              
210             sub iktLink {
211             my ($self, $ikt, $term) = @_;
212              
213             croak('No PSI interface defined') unless $self->{PSI};
214              
215             $ikt = url_encode($ikt);
216             $term = url_encode($term);
217              
218             return $self->{PSI} . "/CMD?ACT=SRCHA&IKT=$ikt&TRM=$term";
219             }
220              
221              
222             sub ppnLink {
223             my ($self, $ppn) = @_;
224              
225             croak('No PSI interface defined') unless $self->{PSI};
226              
227             return $self->{PSI} . "/PPNSET?PPN=$ppn";
228             }
229              
230              
231             sub baseURL {
232             my $self = shift;
233              
234             return $self->{PSI} if $self->{PSI};
235             return $self->{unAPI} if $self->{unAPI};
236             return $self->{SRU} if $self->{SRU};
237              
238             return "";
239             }
240              
241              
242             sub url_encode {
243             my $str = shift;
244             $str =~ s{([^A-Za-z0-9_\.\*])}{sprintf("%%%02x", ord($1))}eg;
245             return $str;
246             }
247              
248              
249             sub url_decode {
250             my $str = shift;
251             $str =~ tr/+/ /;
252             $str =~ s|%([A-Fa-f0-9]{2})|chr(hex($1))|eg;
253             return $str;
254             }
255              
256              
257             # Returns the fully URL-encoded version of the given string as
258             # unicode characters. It does not convert space characters to
259             # '+' characters.
260             # sub url_unicode_encode {
261             # my $str = shift;
262             # $str =~ s{([^A-Za-z0-9_\.\*])}{sprintf("%%u%04x", ord($1))}eg;
263             # return $str;
264             #}
265              
266             1;
267              
268             __END__