]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/ResolverResolver.pm
Use hold current shelf lib to determine availability : opac sorting
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / ResolverResolver.pm
1 #!/usr/bin/perl
2
3 # Copyright (C) 2009-2010 Dan Scott <dscott@laurentian.ca>
4
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18
19 =head1 NAME
20
21 OpenILS::Application::ResolverResolver - retrieves holdings from OpenURL resolvers
22
23 =head1 SYNOPSIS
24
25 Via srfsh:
26   request open-ils.resolver open-ils.resolver.resolve_holdings "issn", "0022-362X"
27 or:
28   request open-ils.resolver open-ils.resolver.resolve_holdings.raw "issn", "0022-362X"
29
30 Via Perl:
31   my $session = OpenSRF::AppSession->create("open-ils.resolver");
32   my $request = $session->request("open-ils.resolver.resolve_holdings", [ "issn", "0022-362X" ] )->gather();
33   $session->disconnect();
34
35   # $request is a reference to the list of hashes
36
37 =head1 DESCRIPTION
38
39 OpenILS::Application::ResolverResolver caches responses from OpenURL resolvers
40 to requests for full-text holdings. Currently integration with SFX is supported.
41
42 Each org_unit can specify a different base URL as the third argument to
43 resolve_holdings(). Eventually org_units will have org_unit settings to hold
44 their resolver type and base URL.
45
46 =head1 AUTHOR
47
48 Dan Scott, dscott@laurentian.ca
49
50 =cut
51
52 package OpenILS::Application::ResolverResolver;
53
54 use strict;
55 use warnings;
56 use LWP::UserAgent;
57 use XML::LibXML;
58
59 # All OpenSRF applications must be based on OpenSRF::Application or
60 # a subclass thereof.  Makes sense, eh?
61 use OpenILS::Application;
62 use base qw/OpenILS::Application/;
63
64 # This is the client class, used for connecting to open-ils.storage
65 use OpenSRF::AppSession;
66
67 # This is an extension of Error.pm that supplies some error types to throw
68 use OpenSRF::EX qw(:try);
69
70 # This is a helper class for querying the OpenSRF Settings application ...
71 use OpenSRF::Utils::SettingsClient;
72
73 # ... and here we have the built in logging helper ...
74 use OpenSRF::Utils::Logger qw($logger);
75
76 # ... and this manages cached results for us ...
77 use OpenSRF::Utils::Cache;
78
79 # ... and this gives us access to the Fieldmapper
80 use OpenILS::Utils::Fieldmapper;
81
82 my $prefix = "open-ils.resolver_"; # Prefix for caching values
83 my $cache;
84 my $cache_timeout;
85 my $default_url_base;              # Default resolver location
86 my $resolver_type;              # Default resolver type
87
88 our ($ua, $parser);
89
90
91 sub initialize {
92     $cache = OpenSRF::Utils::Cache->new('global');
93     my $sclient = OpenSRF::Utils::SettingsClient->new();
94     $cache_timeout = $sclient->config_value(
95         "apps", "open-ils.resolver", "app_settings", "cache_timeout" ) || 300;
96     $default_url_base = $sclient->config_value(
97         "apps", "open-ils.resolver", "app_settings", "default_url_base");
98     $resolver_type = $sclient->config_value(
99         "apps", "open-ils.resolver", "app_settings", "resolver_type");
100 }
101
102 sub child_init {
103     # We need a User Agent to speak to the SFX beast
104     $ua = new LWP::UserAgent;
105     $ua->agent('SameOrigin/1.0');
106
107     # SFX returns XML to us; let us parse
108     $parser = new XML::LibXML;
109 }
110
111 sub resolve_holdings {
112     my $self = shift;
113     my $conn = shift;
114     my $id_type = shift;      # keep it simple for now, either 'issn' or 'isbn'
115     my $id_value = shift;     # the normalized ISSN or ISBN
116     my $url_base = shift || $default_url_base; 
117
118     if ($resolver_type eq 'cufts') {
119         return cufts_holdings($self,$conn,$id_type,$id_value,$url_base);
120     } else {
121         return sfx_holdings($self,$conn,$id_type,$id_value,$url_base);
122     }
123 }
124
125 sub cufts_holdings{
126
127     my $self = $_[0];
128     my $conn = $_[1];
129     my $id_type = $_[2];
130     my $id_value = $_[3];
131     my $url_base = $_[4];
132
133     # We'll use this in our cache key
134     my $method = $self->api_name;
135
136     # We might want to return raw JSON for speedier responses
137     my $format = 'fieldmapper';
138     if ($self->api_name =~ /raw$/) {
139         $format = 'raw';
140     }
141
142     # Nice little CUFTS OpenURL request
143     my $url_args = '?';
144
145     if ($id_type eq 'issn') {
146         $url_args .= "&issn=$id_value";
147     } elsif ($id_type eq 'isbn') {
148         $url_args .= "&isbn=$id_value";
149     }
150     
151     my $ckey = $prefix . $method . $url_base . $id_type . $id_value; 
152
153     # Check the cache to see if we've already looked this up
154     # If we have, shortcut our return value
155     my $result = $cache->get_cache($ckey) || undef;
156     if ($result) {
157         $logger->info("Resolver found a cache hit");    
158         return $result;
159     }
160
161     # Otherwise, let's go and grab the info from the CUFTS server
162     my $req = HTTP::Request->new('GET', "$url_base$url_args");
163
164     # Let's see what we we're trying to request
165     $logger->info("Resolving the following request: $url_base$url_args");
166
167     my $res = $ua->request($req);
168
169     my $xml = $res->content;
170     my $parsed_cufts = $parser->parse_string($xml);
171
172     my (@targets) = $parsed_cufts->findnodes('/CUFTS/resource/service[@name="journal"]');
173
174     my @cufts_result;
175     foreach my $target (@targets) {
176         my %full_txt;
177
178         # Ensure we have a name and especially URL to return
179         $full_txt{'name'} = $target->findvalue('../@name[1]');
180         $full_txt{'url'} = $target->findvalue('./result/url') || next;
181         $full_txt{'coverage'} = $target->findvalue('./result/ft_start_date') . ' - ' . $target->findvalue('./result/ft_end_date');
182         my $embargo = "";
183         my $days_embargo = $target->findvalue('./result/embargo_days') || '';
184         if (length($days_embargo) > 0) {
185             $days_embargo = $days_embargo . " days ";
186         }
187         my $months_embargo = $target->findvalue('./result/embargo_months') || '';
188         if (length($months_embargo) > 0) {
189             $months_embargo = $months_embargo . " months ";
190         }
191         my $years_embargo = $target->findvalue('./result/embargo_years') || '';
192         if (length($years_embargo) > 0) {
193             $years_embargo = $years_embargo . " years ";
194         }
195         if (length($years_embargo . $months_embargo . $days_embargo) > 0) {
196             $embargo = "(most recent " . $years_embargo . $months_embargo . $days_embargo . "unavailable due to publisher restrictions)";
197         }
198         $full_txt{'embargo'} = $embargo;
199
200         if ($format eq 'raw') {
201             push @cufts_result, {
202                 public_name => $full_txt{'name'},
203                 target_url => $full_txt{'url'},
204                 target_coverage => $full_txt{'coverage'},
205                 target_embargo => $full_txt{'embargo'},
206             };
207         } else {
208             my $rhr = Fieldmapper::resolver::holdings_record->new;
209             $rhr->public_name($full_txt{'name'});
210             $rhr->target_url($full_txt{'url'});
211             $rhr->target_coverage($full_txt{'coverage'});
212             $rhr->target_embargo($full_txt{'embargo'});
213             push @cufts_result, $rhr;
214         }
215     }
216
217     # Stuff this into the cache
218     $cache->put_cache($ckey, \@cufts_result, $cache_timeout);
219     
220     # Don't return the list unless it contains results
221     if (scalar(@cufts_result)) {
222         return \@cufts_result;
223     }
224
225     return undef;
226 }
227
228 sub sfx_holdings{
229
230     my $self = $_[0];
231     my $conn = $_[1];
232     my $id_type = $_[2];
233     my $id_value = $_[3];
234     my $url_base = $_[4];
235
236     # We'll use this in our cache key
237     my $method = $self->api_name;
238
239     # We might want to return raw JSON for speedier responses
240     my $format = 'fieldmapper';
241     if ($self->api_name =~ /raw$/) {
242         $format = 'raw';
243     }
244
245     # Big ugly SFX OpenURL request
246     my $url_args = '?url_ver=Z39.88-2004&url_ctx_fmt=infofi/fmt:kev:mtx:ctx&'
247         . 'ctx_enc=UTF-8&ctx_ver=Z39.88-2004&rfr_id=info:sid/evergreen&'
248         . 'sfx.ignore_date_threshold=1&'
249         . 'sfx.response_type=multi_obj_detailed_xml&__service_type=getFullTxt';
250
251     if ($id_type eq 'issn') {
252         $url_args .= "&rft.issn=$id_value";
253     } elsif ($id_type eq 'isbn') {
254         $url_args .= "&rft.isbn=$id_value";
255     }
256     
257     my $ckey = $prefix . $method . $url_base . $id_type . $id_value;
258
259     # Check the cache to see if we've already looked this up
260     # If we have, shortcut our return value
261     my $result = $cache->get_cache($ckey) || undef;
262     if ($result) {
263         $logger->info("Resolver found a cache hit");    
264         return $result;
265     }
266
267     # Otherwise, let's go and grab the info from the SFX server
268     my $req = HTTP::Request->new('GET', "$url_base$url_args");
269
270     # Let's see what we we're trying to request
271     $logger->info("Resolving the following request: $url_base$url_args");
272
273     my $res = $ua->request($req);
274
275     my $xml = $res->content;
276     my $parsed_sfx = $parser->parse_string($xml);
277
278     my (@targets) = $parsed_sfx->findnodes('//target');
279
280     my @sfx_result;
281     foreach my $target (@targets) {
282         my %full_txt;
283
284         # Ensure we have a name and especially URL to return
285         $full_txt{'name'} = $target->findvalue('./target_public_name') || next;
286         $full_txt{'url'} = $target->findvalue('.//target_url') || next;
287         $full_txt{'coverage'} = $target->findvalue('.//coverage_statement') || '';
288         $full_txt{'embargo'} = $target->findvalue('.//embargo_statement') || '';
289
290         if ($format eq 'raw') {
291             push @sfx_result, {
292                 public_name => $full_txt{'name'},
293                 target_url => $full_txt{'url'},
294                 target_coverage => $full_txt{'coverage'},
295                 target_embargo => $full_txt{'embargo'},
296             };
297         } else {
298             my $rhr = Fieldmapper::resolver::holdings_record->new;
299             $rhr->public_name($full_txt{'name'});
300             $rhr->target_url($full_txt{'url'});
301             $rhr->target_coverage($full_txt{'coverage'});
302             $rhr->target_embargo($full_txt{'embargo'});
303             push @sfx_result, $rhr;
304         }
305     }
306
307     # Stuff this into the cache
308     $cache->put_cache($ckey, \@sfx_result, $cache_timeout);
309     
310     # Don't return the list unless it contains results
311     if (scalar(@sfx_result)) {
312         return \@sfx_result;
313     }
314
315     return undef;
316 }
317
318 __PACKAGE__->register_method(
319     method    => 'resolve_holdings',
320     api_name  => 'open-ils.resolver.resolve_holdings',
321     api_level => 1,
322     argc      => 3,
323     signature => {
324         desc     => <<"         DESC",
325 Returns a list of "rhr" objects representing the full-text holdings for a given ISBN or ISSN
326          DESC
327         'params' => [ {
328                 name => 'id_type',
329                 desc => 'The type of identifier ("issn" or "isbn")',
330                 type => 'string' 
331             }, {
332                 name => 'id_value',
333                 desc => 'The identifier value',
334                 type => 'string'
335             }, {
336                  name => 'url_base',
337                  desc => 'The base URL for the resolver and instance',
338                  type => 'string'
339             },
340         ],
341         'return' => {
342             desc => 'Returns a list of "rhr" objects representing the full-text holdings for a given ISBN or ISSN',
343             type => 'array'
344         }
345     }
346 );
347
348 __PACKAGE__->register_method(
349     method    => 'resolve_holdings',
350     api_name  => 'open-ils.resolver.resolve_holdings.raw',
351     api_level => 1,
352     argc      => 3,
353     signature => {
354         desc     => <<"         DESC",
355 Returns a list of raw JSON objects representing the full-text holdings for a given ISBN or ISSN
356          DESC
357         'params' => [ {
358                 name => 'id_type',
359                 desc => 'The type of identifier ("issn" or "isbn")',
360                 type => 'string' 
361             }, {
362                 name => 'id_value',
363                 desc => 'The identifier value',
364                 type => 'string'
365             }, {
366                  name => 'url_base',
367                  desc => 'The base URL for the resolver and instance',
368                  type => 'string'
369             },
370         ],
371         'return' => {
372             desc => 'Returns a list of raw JSON objects representing the full-text holdings for a given ISBN or ISSN',
373             type => 'array'
374         }
375     }
376 );
377
378 # Clear cache for specific lookups
379 sub delete_cached_holdings {
380     my $self = shift;
381     my $conn = shift;
382     my $id_type = shift;      # keep it simple for now, either 'issn' or 'isbn'
383     my $id_value = shift;     # the normalized ISSN or ISBN
384     my $url_base = shift || $default_url_base; 
385     my @deleted_keys;
386
387     $logger->warn("Deleting value [$id_value]");
388     # We'll use this in our cache key
389     foreach my $method ('open-ils.resolver.resolve_holdings.raw', 'open-ils.resolver.resolve_holdings') {
390         my $ckey = $prefix . $method . $url_base . $id_type . $id_value;
391
392         $logger->warn("Deleted cache key [$ckey]");
393         my $result = $cache->delete_cache($ckey);
394
395         $logger->warn("Result of deleting cache key: [$result]");
396         push @deleted_keys, $result;
397     }
398
399     return \@deleted_keys;
400 }
401
402 __PACKAGE__->register_method(
403     method    => 'delete_cached_holdings',
404     api_name  => 'open-ils.resolver.delete_cached_holdings',
405     api_level => 1,
406     argc      => 3,
407     signature => {
408         desc     => <<"         DESC",
409 Deletes the cached value of the full-text holdings for a given ISBN or ISSN
410          DESC
411         'params' => [ {
412                 name => 'id_type',
413                 desc => 'The type of identifier ("issn" or "isbn")',
414                 type => 'string' 
415             }, {
416                 name => 'id_value',
417                 desc => 'The identifier value',
418                 type => 'string'
419             }, {
420                  name => 'url_base',
421                  desc => 'The base URL for the resolver and instance',
422                  type => 'string'
423             },
424         ],
425         'return' => {
426             desc => 'Deletes the cached value of the full-text holdings for a given ISBN or ISSN',
427             type => 'array'
428         }
429     }
430 );
431
432
433 1;