]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/ResolverResolver.pm
LP#1849212: (follow-up) numerous fixes to open-ils.courses.detach_material
[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 my $default_request_timeout;                    # Default browser timeout
88
89 our ($ua, $parser);
90
91
92 sub initialize {
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;
104 }
105
106 sub child_init {
107
108     # We need a User Agent to speak to the SFX beast
109     $ua = new LWP::UserAgent;
110     $ua->agent('SameOrigin/1.0');
111
112     # SFX returns XML to us; let us parse
113     $parser = new XML::LibXML;
114 }
115
116 sub resolve_holdings {
117     my $self = shift;
118     my $conn = shift;
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; 
123
124     if (!$id_type) {
125         $logger->warn("Resolver was not given an ID type to resolve");
126         return;
127     }
128     if (!$id_value) {
129         $logger->warn("Resolver was not given an ID value to resolve");
130         return;
131     }
132
133     # Need some sort of timeout in case resolver is unreachable
134     $ua->timeout($request_timeout);
135
136     if ($resolver_type eq 'cufts') {
137         return cufts_holdings($self,$conn,$id_type,$id_value,$url_base);
138     } else {
139         return sfx_holdings($self,$conn,$id_type,$id_value,$url_base);
140     }
141 }
142
143 sub cufts_holdings{
144
145     my $self = $_[0];
146     my $conn = $_[1];
147     my $id_type = $_[2];
148     my $id_value = $_[3];
149     my $url_base = $_[4];
150
151     # We'll use this in our cache key
152     my $method = $self->api_name;
153
154     # We might want to return raw JSON for speedier responses
155     my $format = 'fieldmapper';
156     if ($self->api_name =~ /raw$/) {
157         $format = 'raw';
158     }
159
160     # Nice little CUFTS OpenURL request
161     my $url_args = '?';
162
163     if ($id_type eq 'issn') {
164         $url_args .= "&issn=$id_value";
165     } elsif ($id_type eq 'isbn') {
166         $url_args .= "&isbn=$id_value";
167     }
168     
169     my $ckey = $prefix . $method . $url_base . $id_type . $id_value; 
170
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;
174     if ($result) {
175         $logger->info("Resolver found a cache hit");    
176         return $result;
177     }
178
179     my $res = undef;
180
181     # Let's see what we we're trying to request
182     $logger->info("Resolving the following request: $url_base$url_args");
183
184     # We attempt to deal with potential problems in request
185     eval {
186         $res = $ua->get("$url_base$url_args"); 
187     } or do {
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.');
191     };
192
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.');
197     }
198
199     my $xml = $res->content;
200     my $parsed_cufts = $parser->parse_string($xml);
201
202     my (@targets) = $parsed_cufts->findnodes('/CUFTS/resource/service[@name="journal"]');
203
204     my @cufts_result;
205     foreach my $target (@targets) {
206         my %full_txt;
207
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');
212         my $embargo = "";
213         my $days_embargo = $target->findvalue('./result/embargo_days') || '';
214         if (length($days_embargo) > 0) {
215             $days_embargo = $days_embargo . " days ";
216         }
217         my $months_embargo = $target->findvalue('./result/embargo_months') || '';
218         if (length($months_embargo) > 0) {
219             $months_embargo = $months_embargo . " months ";
220         }
221         my $years_embargo = $target->findvalue('./result/embargo_years') || '';
222         if (length($years_embargo) > 0) {
223             $years_embargo = $years_embargo . " years ";
224         }
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)";
227         }
228         $full_txt{'embargo'} = $embargo;
229
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'},
236             };
237         } else {
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;
244         }
245     }
246
247     # Stuff this into the cache
248     $cache->put_cache($ckey, \@cufts_result, $cache_timeout);
249     
250     # Don't return the list unless it contains results
251     if (scalar(@cufts_result)) {
252         return \@cufts_result;
253     }
254
255     return undef;
256 }
257
258 sub sfx_holdings{
259
260     my $self = $_[0];
261     my $conn = $_[1];
262     my $id_type = $_[2];
263     my $id_value = $_[3];
264     my $url_base = $_[4];
265
266     # We'll use this in our cache key
267     my $method = $self->api_name;
268
269     # We might want to return raw JSON for speedier responses
270     my $format = 'fieldmapper';
271     if ($self->api_name =~ /raw$/) {
272         $format = 'raw';
273     }
274
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';
280
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";
285     }
286     
287     my $ckey = $prefix . $method . $url_base . $id_type . $id_value;
288
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;
292     if ($result) {
293         $logger->info("Resolver found a cache hit");    
294         return $result;
295     }
296
297     my $res = undef;
298
299     # Let's see what we we're trying to request
300     $logger->info("Resolving the following request: $url_base$url_args");
301
302     # We attempt to deal with potential problems in request
303     eval {
304         $res = $ua->get("$url_base$url_args"); 
305     } or do {
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.');
309     };
310
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.');
315     }
316
317     # All clear
318     my $xml = $res->content;
319     my $parsed_sfx = $parser->parse_string($xml);
320
321     my (@targets) = $parsed_sfx->findnodes('//target');
322
323     my @sfx_result;
324     foreach my $target (@targets) {
325         my %full_txt;
326
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') || '';
332
333         if ($format eq 'raw') {
334             push @sfx_result, {
335                 public_name => $full_txt{'name'},
336                 target_url => $full_txt{'url'},
337                 target_coverage => $full_txt{'coverage'},
338                 target_embargo => $full_txt{'embargo'},
339             };
340         } else {
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;
347         }
348     }
349
350     # Stuff this into the cache
351     $cache->put_cache($ckey, \@sfx_result, $cache_timeout);
352     
353     # Don't return the list unless it contains results
354     if (scalar(@sfx_result)) {
355         return \@sfx_result;
356     }
357
358     return undef;
359 }
360
361 # This uses the resolver structure for passing back a link directly to the resolver
362 sub bow_out_gracefully {
363     my $alt_url = $_[0];
364     my $reason = $_[1];
365
366     my @sfx_result;
367                 
368     push @sfx_result, {
369         public_name => "Online holdings",
370         target_url => $alt_url,
371         target_coverage => $reason,
372         target_embargo => "",
373     };
374    
375     return \@sfx_result;
376 }
377
378 __PACKAGE__->register_method(
379     method    => 'resolve_holdings',
380     api_name  => 'open-ils.resolver.resolve_holdings',
381     api_level => 1,
382     argc      => 3,
383     signature => {
384         desc     => <<"         DESC",
385 Returns a list of "rhr" objects representing the full-text holdings for a given ISBN or ISSN
386          DESC
387         'params' => [ {
388                 name => 'id_type',
389                 desc => 'The type of identifier ("issn" or "isbn")',
390                 type => 'string' 
391             }, {
392                 name => 'id_value',
393                 desc => 'The identifier value',
394                 type => 'string'
395             }, {
396                  name => 'url_base',
397                  desc => 'The base URL for the resolver and instance',
398                  type => 'string'
399             }, {
400                  name => 'request_timeout',
401                  desc => 'The timeout for the HTTP request',
402                  type => 'string'
403             },
404         ],
405         'return' => {
406             desc => 'Returns a list of "rhr" objects representing the full-text holdings for a given ISBN or ISSN',
407             type => 'array'
408         }
409     }
410 );
411
412 __PACKAGE__->register_method(
413     method    => 'resolve_holdings',
414     api_name  => 'open-ils.resolver.resolve_holdings.raw',
415     api_level => 1,
416     argc      => 3,
417     signature => {
418         desc     => <<"         DESC",
419 Returns a list of raw JSON objects representing the full-text holdings for a given ISBN or ISSN
420          DESC
421         'params' => [ {
422                 name => 'id_type',
423                 desc => 'The type of identifier ("issn" or "isbn")',
424                 type => 'string' 
425             }, {
426                 name => 'id_value',
427                 desc => 'The identifier value',
428                 type => 'string'
429             }, {
430                  name => 'url_base',
431                  desc => 'The base URL for the resolver and instance',
432                  type => 'string'
433             }, {
434                  name => 'request_timeout',
435                  desc => 'The timeout for the HTTP request',
436                  type => 'string'
437             },
438         ],
439         'return' => {
440             desc => 'Returns a list of raw JSON objects representing the full-text holdings for a given ISBN or ISSN',
441             type => 'array'
442         }
443     }
444 );
445
446 # Clear cache for specific lookups
447 sub delete_cached_holdings {
448     my $self = shift;
449     my $conn = shift;
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; 
453     my @deleted_keys;
454
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;
459
460         $logger->warn("Deleted cache key [$ckey]");
461         my $result = $cache->delete_cache($ckey);
462
463         $logger->warn("Result of deleting cache key: [$result]");
464         push @deleted_keys, $result;
465     }
466
467     return \@deleted_keys;
468 }
469
470 __PACKAGE__->register_method(
471     method    => 'delete_cached_holdings',
472     api_name  => 'open-ils.resolver.delete_cached_holdings',
473     api_level => 1,
474     argc      => 3,
475     signature => {
476         desc     => <<"         DESC",
477 Deletes the cached value of the full-text holdings for a given ISBN or ISSN
478          DESC
479         'params' => [ {
480                  name => 'url_base',
481                  desc => 'The base URL for the resolver and instance',
482                  type => 'string'
483             }, {
484                 name => 'id_type',
485                 desc => 'The type of identifier ("issn" or "isbn")',
486                 type => 'string'
487             }, {
488                 name => 'id_value',
489                 desc => 'The identifier value',
490                 type => 'string'
491             }
492         ],
493         'return' => {
494             desc => 'Deletes the cached value of the full-text holdings for a given ISBN or ISSN',
495             type => 'array'
496         }
497     }
498 );
499
500
501 1;