]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/AddedContent.pm
Add method to permit AC handler to expect keyhash
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / AddedContent.pm
1 package OpenILS::WWW::AddedContent;
2 use strict; use warnings;
3
4 use CGI;
5 use Apache2::Log;
6 use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND :log);
7 use APR::Const    -compile => qw(:error SUCCESS);
8 use Apache2::RequestRec ();
9 use Apache2::RequestIO ();
10 use Apache2::RequestUtil;
11 use Data::Dumper;
12 use UNIVERSAL::require;
13
14 use OpenSRF::EX qw(:try);
15 use OpenSRF::Utils::Cache;
16 use OpenSRF::System;
17 use OpenSRF::Utils::Logger qw/$logger/;
18 use OpenILS::Utils::CStoreEditor;
19
20 use LWP::UserAgent;
21 use MIME::Base64;
22
23 use Business::ISBN;
24
25 my $AC = __PACKAGE__;
26
27
28 # set the bootstrap config when this module is loaded
29 my $bs_config;
30
31 sub import {
32     my $self = shift;
33     $bs_config = shift;
34 }
35
36
37 my $handler; # added content handler class handle
38 my $cache; # memcache handle
39 my $net_timeout; # max seconds to wait for a response from the added content vendor
40 my $max_errors; # max consecutive lookup failures before added content is temporarily disabled
41 my $error_countdown; # current consecutive errors countdown
42
43 # number of seconds to wait before next lookup 
44 # is attempted after lookups have been disabled
45 my $error_retry_timeout;
46
47
48 sub child_init {
49
50     OpenSRF::System->bootstrap_client( config_file => $bs_config );
51     $cache = OpenSRF::Utils::Cache->new;
52
53     my $sclient = OpenSRF::Utils::SettingsClient->new();
54     my $ac_data = $sclient->config_value("added_content");
55
56     return Apache2::Const::OK unless $ac_data;
57     my $ac_handler = $ac_data->{module};
58     return Apache2::Const::OK unless $ac_handler;
59
60     $net_timeout = $ac_data->{timeout} || 1;
61     $error_countdown = $max_errors = $ac_data->{max_errors} || 10;
62     $error_retry_timeout = $ac_data->{retry_timeout} || 600;
63
64     $logger->debug("Attempting to load Added Content handler: $ac_handler");
65
66     $ac_handler->use;
67
68     if($@) {    
69         $logger->error("Unable to load Added Content handler [$ac_handler]: $@"); 
70         return Apache2::Const::OK; 
71     }
72
73     $handler = $ac_handler->new($ac_data);
74     $logger->debug("added content loaded handler: $handler");
75     return Apache2::Const::OK;
76 }
77
78
79 sub handler {
80
81     my $r   = shift;
82
83     # If the URL requested matches a file on the filesystem, have Apache serve that file
84     # this allows for local content (most typically images) to be used for some requests
85     return Apache2::Const::DECLINED if (-e $r->filename);
86
87     my $cgi = CGI->new;
88     my @path_parts = split( /\//, $r->unparsed_uri );
89
90     # Intended URL formats
91     # /opac/extras/ac/jacket/medium/ISBN_VALUE      -- classic keyed-off-isbn
92     # /opac/extras/ac/-3/-2/-1
93     # /opac/extras/ac/jacket/medium/r/RECORD_ID     -- provide record id (bre.id)
94     # /opac/extras/ac/-4/-3/-2/-1
95     # /opac/extras/ac/jacket/medium/m/RECORD_ID     -- XXX: future use for metarecord id
96
97     my $keytype_in_url = $path_parts[-2];  # if not in one of m, r, this will be the $format
98
99     my $type;
100     my $format;
101     my $keytype;
102     my $keyvalue;
103
104     if ($keytype_in_url =~ m/^(r|m)$/) {
105         $type = $path_parts[-4];
106         $format = $path_parts[-3];
107         $keyvalue = $path_parts[-1]; # a record/metarecord id
108         $keytype = 'record';
109     } else {
110         $type = $path_parts[-3];
111         $format = $path_parts[-2];
112         $keyvalue = $path_parts[-1]; # an isbn
113         $keytype = 'isbn';
114     }
115
116     my $res;
117     my $keyhash;
118     my $cachekey;
119
120     $cachekey = ($keytype eq "isbn") ? $keyvalue : $keytype . '_' . $keyvalue;
121
122     child_init() unless $handler;
123
124     return Apache2::Const::NOT_FOUND unless $handler and $type and $format and $cachekey;
125
126     my $err;
127     my $data;
128     my $method = "${type}_${format}";
129
130     return Apache2::Const::NOT_FOUND unless $handler->can($method);
131     return $res if defined($res = $AC->serve_from_cache($type, $format, $cachekey));
132     return Apache2::Const::NOT_FOUND unless $AC->lookups_enabled;
133
134     if ($keytype eq "isbn") { # if this request uses isbn for the key
135         # craft a hash with the single isbn, because that's all we will have
136         $keyhash = {};
137         $keyhash->{"isbn"} = [$keyvalue];
138     } else {
139         my $key_data = get_rec_keys($keyvalue);
140         my @isbns = grep {$_->{tag} eq '020'} @$key_data;
141         my @upcs = grep {$_->{tag} eq '024'} @$key_data;
142
143         map {
144             my $isbn_obj = Business::ISBN->new($_->{value});
145             my $isbn_str;
146             $isbn_str = $isbn_obj->as_string([]) if defined($isbn_obj);
147             $_->{value} = $isbn_str;
148             undef $_ if !defined($_->{value});
149         } @isbns;
150
151         $keyhash = {
152             isbn => [map {$_->{value}} @isbns],
153             upc => [map {$_->{value}} @upcs]
154         };
155     }
156
157     return Apache2::Const::NOT_FOUND unless @{$keyhash->{isbn}} || @{$keyhash->{upc}};
158
159     try {
160         if ($handler->can('expects_keyhash') && $handler->expects_keyhash() eq 1) {
161             # Handler expects a keyhash
162             $data = $handler->$method($keyhash);
163         } else {
164             # Pass single ISBN as a scalar to the handler
165             $data = $handler->$method($keyhash->{isbn}[0]);
166         }
167     } catch Error with {
168         $err = shift;
169         decr_error_countdown();
170         $logger->debug("added content handler failed: $method($keytype/$keyvalue) => $err"); # XXX: logs unhelpful hashref
171     };
172
173     return Apache2::Const::NOT_FOUND if $err;
174
175     if(!$data) {
176         # if the AC lookup found no corresponding data, cache that information
177         $logger->debug("added content handler returned no results $method($keytype/$keyvalue)") unless $data;
178         $AC->cache_result($type, $format, $cachekey, {nocontent=>1});
179         return Apache2::Const::NOT_FOUND;
180     }
181     
182     $AC->print_content($data);
183     $AC->cache_result($type, $format, $cachekey, $data);
184
185     reset_error_countdown();
186     return Apache2::Const::OK;
187 }
188
189 # returns [{tag => $tag, value => $value}, {tag => $tag2, value => $value2}]
190 sub get_rec_keys {
191     my $id = shift;
192     return OpenILS::Utils::CStoreEditor->new->json_query({
193         select => {mfr => ['tag', 'value']},
194         from => 'mfr',
195         where => {
196             record => $id,
197             '-or' => [
198                 {
199                     '-and' => [
200                         {tag => '020'},
201                         {subfield => 'a'}
202                     ]
203                 }, {
204                     '-and' => [
205                         {tag => '024'},
206                         {subfield => 'a'},
207                         {ind1 => 1}
208                     ]
209                 }
210             ]
211         }
212     });
213 }
214
215 sub print_content {
216     my($class, $data, $from_cache) = @_;
217     return Apache2::Const::NOT_FOUND if $data->{nocontent};
218
219     my $ct = $data->{content_type};
220     my $content = $data->{content};
221     print "Content-type: $ct\n\n";
222
223     if($data->{binary}) {
224         binmode STDOUT;
225         # if it hasn't been cached yet, it's still in binary form
226         print( ($from_cache) ? decode_base64($content) : $content );
227     } else {
228         print $content;
229     }
230
231
232     return Apache2::Const::OK;
233 }
234
235
236
237
238 # returns an HTPP::Response object
239 sub get_url {
240     my( $self, $url ) = @_;
241
242     $logger->info("added content getting [timeout=$net_timeout, errors_remaining=$error_countdown] URL = $url");
243     my $agent = LWP::UserAgent->new(timeout => $net_timeout);
244
245     my $res = $agent->get($url); 
246     $logger->info("added content request returned with code " . $res->code);
247     die "added content request failed: " . $res->status_line ."\n" unless $res->is_success;
248
249     return $res;
250 }
251
252 sub lookups_enabled {
253     if( $cache->get_cache('ac.no_lookup') ) {
254         $logger->info("added content lookup disabled");
255         return undef;
256     }
257     return 1;
258 }
259
260 sub disable_lookups {
261     $cache->put_cache('ac.no_lookup', 1, $error_retry_timeout);
262 }
263
264 sub decr_error_countdown {
265     $error_countdown--;
266     if($error_countdown < 1) {
267         $logger->warn("added content error count exhausted.  Disabling lookups for $error_retry_timeout seconds");
268         $AC->disable_lookups;
269     }
270 }
271
272 sub reset_error_countdown {
273     $error_countdown = $max_errors;
274 }
275
276 sub cache_result {
277     my($class, $type, $format, $key, $data) = @_;
278     $logger->debug("caching $type/$format/$key");
279     $data->{content} = encode_base64($data->{content}) if $data->{binary};
280     return $cache->put_cache("ac.$type.$format.$key", $data);
281 }
282
283 sub serve_from_cache {
284     my($class, $type, $format, $key) = @_;
285     my $data = $cache->get_cache("ac.$type.$format.$key");
286     return undef unless $data;
287     $logger->debug("serving $type/$format/$key from cache");
288     return $class->print_content($data, 1);
289 }
290
291
292
293 1;