3 # Copyright (C) 2009-2010 Dan Scott <dscott@laurentian.ca>
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.
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.
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.
21 OpenILS::Application::ResolverResolver - retrieves holdings from OpenURL resolvers
26 request open-ils.resolver open-ils.resolver.resolve_holdings "issn", "0022-362X"
28 request open-ils.resolver open-ils.resolver.resolve_holdings.raw "issn", "0022-362X"
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();
35 # $request is a reference to the list of hashes
39 OpenILS::Application::ResolverResolver caches responses from OpenURL resolvers
40 to requests for full-text holdings. Currently integration with SFX is supported.
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.
48 Dan Scott, dscott@laurentian.ca
52 package OpenILS::Application::ResolverResolver;
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/;
64 # This is the client class, used for connecting to open-ils.storage
65 use OpenSRF::AppSession;
67 # This is an extension of Error.pm that supplies some error types to throw
68 use OpenSRF::EX qw(:try);
70 # This is a helper class for querying the OpenSRF Settings application ...
71 use OpenSRF::Utils::SettingsClient;
73 # ... and here we have the built in logging helper ...
74 use OpenSRF::Utils::Logger qw($logger);
76 # ... and this manages cached results for us ...
77 use OpenSRF::Utils::Cache;
79 # ... and this gives us access to the Fieldmapper
80 use OpenILS::Utils::Fieldmapper;
82 my $prefix = "open-ils.resolver_"; # Prefix for caching values
85 my $default_url_base; # Default resolver location
86 my $resolver_type; # Default resolver type
87 my $default_request_timeout; # Default browser timeout
93 $cache = OpenSRF::Utils::Cache->new('global');
94 my $sclient = OpenSRF::Utils::SettingsClient->new();
95 $cache_timeout = $sclient->config_value(
96 "apps", "open-ils.resolver", "app_settings", "cache_timeout" ) || 300;
97 $default_url_base = $sclient->config_value(
98 "apps", "open-ils.resolver", "app_settings", "default_url_base");
99 $resolver_type = $sclient->config_value(
100 "apps", "open-ils.resolver", "app_settings", "resolver_type") || 'sfx';
101 # We set a browser timeout
102 $default_request_timeout = $sclient->config_value(
103 "apps", "open-ils.resolver", "app_settings", "request_timeout" ) || 60;
108 # We need a User Agent to speak to the SFX beast
109 $ua = new LWP::UserAgent;
110 $ua->agent('SameOrigin/1.0');
112 # SFX returns XML to us; let us parse
113 $parser = new XML::LibXML;
116 sub resolve_holdings {
119 my $id_type = shift; # keep it simple for now, either 'issn' or 'isbn'
120 my $id_value = shift; # the normalized ISSN or ISBN
121 my $url_base = shift || $default_url_base;
122 my $request_timeout = shift || $default_request_timeout;
125 $logger->warn("Resolver was not given an ID type to resolve");
129 $logger->warn("Resolver was not given an ID value to resolve");
133 # Need some sort of timeout in case resolver is unreachable
134 $ua->timeout($request_timeout);
136 if ($resolver_type eq 'cufts') {
137 return cufts_holdings($self,$conn,$id_type,$id_value,$url_base);
139 return sfx_holdings($self,$conn,$id_type,$id_value,$url_base);
148 my $id_value = $_[3];
149 my $url_base = $_[4];
151 # We'll use this in our cache key
152 my $method = $self->api_name;
154 # We might want to return raw JSON for speedier responses
155 my $format = 'fieldmapper';
156 if ($self->api_name =~ /raw$/) {
160 # Nice little CUFTS OpenURL request
163 if ($id_type eq 'issn') {
164 $url_args .= "&issn=$id_value";
165 } elsif ($id_type eq 'isbn') {
166 $url_args .= "&isbn=$id_value";
169 my $ckey = $prefix . $method . $url_base . $id_type . $id_value;
171 # Check the cache to see if we've already looked this up
172 # If we have, shortcut our return value
173 my $result = $cache->get_cache($ckey) || undef;
175 $logger->info("Resolver found a cache hit");
181 # Let's see what we we're trying to request
182 $logger->info("Resolving the following request: $url_base$url_args");
184 # We attempt to deal with potential problems in request
186 $res = $ua->get("$url_base$url_args");
188 $logger->info("execution error");
189 return bow_out_gracefully("$url_base?ctx_ver=Z39.88-2004&rft.$id_type=$id_value",
190 'Check link for additional holdings information.');
193 if ($res->status_line =~ /timeout/) {
194 $logger->info("timeout error");
195 return bow_out_gracefully("$url_base?ctx_ver=Z39.88-2004&rft.$id_type=$id_value",
196 'Check link for additional holdings information.');
199 my $xml = $res->content;
200 my $parsed_cufts = $parser->parse_string($xml);
202 my (@targets) = $parsed_cufts->findnodes('/CUFTS/resource/service[@name="journal"]');
205 foreach my $target (@targets) {
208 # Ensure we have a name and especially URL to return
209 $full_txt{'name'} = $target->findvalue('../@name[1]');
210 $full_txt{'url'} = $target->findvalue('./result/url') || next;
211 $full_txt{'coverage'} = $target->findvalue('./result/ft_start_date') . ' - ' . $target->findvalue('./result/ft_end_date');
213 my $days_embargo = $target->findvalue('./result/embargo_days') || '';
214 if (length($days_embargo) > 0) {
215 $days_embargo = $days_embargo . " days ";
217 my $months_embargo = $target->findvalue('./result/embargo_months') || '';
218 if (length($months_embargo) > 0) {
219 $months_embargo = $months_embargo . " months ";
221 my $years_embargo = $target->findvalue('./result/embargo_years') || '';
222 if (length($years_embargo) > 0) {
223 $years_embargo = $years_embargo . " years ";
225 if (length($years_embargo . $months_embargo . $days_embargo) > 0) {
226 $embargo = "(most recent " . $years_embargo . $months_embargo . $days_embargo . "unavailable due to publisher restrictions)";
228 $full_txt{'embargo'} = $embargo;
230 if ($format eq 'raw') {
231 push @cufts_result, {
232 public_name => $full_txt{'name'},
233 target_url => $full_txt{'url'},
234 target_coverage => $full_txt{'coverage'},
235 target_embargo => $full_txt{'embargo'},
238 my $rhr = Fieldmapper::resolver::holdings_record->new;
239 $rhr->public_name($full_txt{'name'});
240 $rhr->target_url($full_txt{'url'});
241 $rhr->target_coverage($full_txt{'coverage'});
242 $rhr->target_embargo($full_txt{'embargo'});
243 push @cufts_result, $rhr;
247 # Stuff this into the cache
248 $cache->put_cache($ckey, \@cufts_result, $cache_timeout);
250 # Don't return the list unless it contains results
251 if (scalar(@cufts_result)) {
252 return \@cufts_result;
263 my $id_value = $_[3];
264 my $url_base = $_[4];
266 # We'll use this in our cache key
267 my $method = $self->api_name;
269 # We might want to return raw JSON for speedier responses
270 my $format = 'fieldmapper';
271 if ($self->api_name =~ /raw$/) {
275 # Big ugly SFX OpenURL request
276 my $url_args = '?url_ver=Z39.88-2004&url_ctx_fmt=infofi/fmt:kev:mtx:ctx&'
277 . 'ctx_enc=UTF-8&ctx_ver=Z39.88-2004&rfr_id=info:sid/evergreen&'
278 . 'sfx.ignore_date_threshold=1&'
279 . 'sfx.response_type=multi_obj_detailed_xml&__service_type=getFullTxt';
281 if ($id_type eq 'issn') {
282 $url_args .= "&rft.issn=$id_value";
283 } elsif ($id_type eq 'isbn') {
284 $url_args .= "&rft.isbn=$id_value";
287 my $ckey = $prefix . $method . $url_base . $id_type . $id_value;
289 # Check the cache to see if we've already looked this up
290 # If we have, shortcut our return value
291 my $result = $cache->get_cache($ckey) || undef;
293 $logger->info("Resolver found a cache hit");
299 # Let's see what we we're trying to request
300 $logger->info("Resolving the following request: $url_base$url_args");
302 # We attempt to deal with potential problems in request
304 $res = $ua->get("$url_base$url_args");
306 $logger->info("execution error");
307 return bow_out_gracefully("$url_base?ctx_ver=Z39.88-2004&rft.$id_type=$id_value",
308 'Check link for additional holdings information.');
311 if ($res->status_line =~ /timeout/) {
312 $logger->info("timeout error");
313 return bow_out_gracefully("$url_base?ctx_ver=Z39.88-2004&rft.$id_type=$id_value",
314 'Check link for additional holdings information.');
318 my $xml = $res->content;
319 my $parsed_sfx = $parser->parse_string($xml);
321 my (@targets) = $parsed_sfx->findnodes('//target');
324 foreach my $target (@targets) {
327 # Ensure we have a name and especially URL to return
328 $full_txt{'name'} = $target->findvalue('./target_public_name') || next;
329 $full_txt{'url'} = $target->findvalue('.//target_url') || next;
330 $full_txt{'coverage'} = $target->findvalue('.//coverage_statement') || '';
331 $full_txt{'embargo'} = $target->findvalue('.//embargo_statement') || '';
333 if ($format eq 'raw') {
335 public_name => $full_txt{'name'},
336 target_url => $full_txt{'url'},
337 target_coverage => $full_txt{'coverage'},
338 target_embargo => $full_txt{'embargo'},
341 my $rhr = Fieldmapper::resolver::holdings_record->new;
342 $rhr->public_name($full_txt{'name'});
343 $rhr->target_url($full_txt{'url'});
344 $rhr->target_coverage($full_txt{'coverage'});
345 $rhr->target_embargo($full_txt{'embargo'});
346 push @sfx_result, $rhr;
350 # Stuff this into the cache
351 $cache->put_cache($ckey, \@sfx_result, $cache_timeout);
353 # Don't return the list unless it contains results
354 if (scalar(@sfx_result)) {
361 # This uses the resolver structure for passing back a link directly to the resolver
362 sub bow_out_gracefully {
369 public_name => "Online holdings",
370 target_url => $alt_url,
371 target_coverage => $reason,
372 target_embargo => "",
378 __PACKAGE__->register_method(
379 method => 'resolve_holdings',
380 api_name => 'open-ils.resolver.resolve_holdings',
385 Returns a list of "rhr" objects representing the full-text holdings for a given ISBN or ISSN
389 desc => 'The type of identifier ("issn" or "isbn")',
393 desc => 'The identifier value',
397 desc => 'The base URL for the resolver and instance',
400 name => 'request_timeout',
401 desc => 'The timeout for the HTTP request',
406 desc => 'Returns a list of "rhr" objects representing the full-text holdings for a given ISBN or ISSN',
412 __PACKAGE__->register_method(
413 method => 'resolve_holdings',
414 api_name => 'open-ils.resolver.resolve_holdings.raw',
419 Returns a list of raw JSON objects representing the full-text holdings for a given ISBN or ISSN
423 desc => 'The type of identifier ("issn" or "isbn")',
427 desc => 'The identifier value',
431 desc => 'The base URL for the resolver and instance',
434 name => 'request_timeout',
435 desc => 'The timeout for the HTTP request',
440 desc => 'Returns a list of raw JSON objects representing the full-text holdings for a given ISBN or ISSN',
446 # Clear cache for specific lookups
447 sub delete_cached_holdings {
450 my $id_type = shift; # keep it simple for now, either 'issn' or 'isbn'
451 my $id_value = shift; # the normalized ISSN or ISBN
452 my $url_base = shift || $default_url_base;
455 $logger->warn("Deleting value [$id_value]");
456 # We'll use this in our cache key
457 foreach my $method ('open-ils.resolver.resolve_holdings.raw', 'open-ils.resolver.resolve_holdings') {
458 my $ckey = $prefix . $method . $url_base . $id_type . $id_value;
460 $logger->warn("Deleted cache key [$ckey]");
461 my $result = $cache->delete_cache($ckey);
463 $logger->warn("Result of deleting cache key: [$result]");
464 push @deleted_keys, $result;
467 return \@deleted_keys;
470 __PACKAGE__->register_method(
471 method => 'delete_cached_holdings',
472 api_name => 'open-ils.resolver.delete_cached_holdings',
477 Deletes the cached value of the full-text holdings for a given ISBN or ISSN
481 desc => 'The base URL for the resolver and instance',
485 desc => 'The type of identifier ("issn" or "isbn")',
489 desc => 'The identifier value',
494 desc => 'Deletes the cached value of the full-text holdings for a given ISBN or ISSN',