]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
LP#1254146 gracefully handle custom trees sans entries
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / WWW / EGCatLoader / Util.pm
1 package OpenILS::WWW::EGCatLoader;
2 use strict; use warnings;
3 use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
4 use File::Spec;
5 use Time::HiRes qw/time sleep/;
6 use OpenSRF::Utils::Cache;
7 use OpenSRF::Utils::Logger qw/$logger/;
8 use OpenILS::Utils::CStoreEditor qw/:funcs/;
9 use OpenILS::Utils::Fieldmapper;
10 use OpenILS::Application::AppUtils;
11 use OpenSRF::MultiSession;
12
13 my $U = 'OpenILS::Application::AppUtils';
14
15 my $ro_object_subs; # cached subs
16 our %cache = ( # cached data
17     map => {en_us => {}},
18     list => {en_us => {}},
19     search => {en_us => {}},
20     org_settings => {en_us => {}},
21     search_filter_groups => {en_us => {}},
22     aou_tree => {en_us => undef},
23     aouct_tree => {},
24     eg_cache_hash => undef
25 );
26
27 sub init_ro_object_cache {
28     my $self = shift;
29     my $e = $self->editor;
30     my $ctx = $self->ctx;
31
32     # reset org unit setting cache on each page load to avoid the
33     # requirement of reloading apache with each org-setting change
34     $cache{org_settings} = {};
35
36     if($ro_object_subs) {
37         # subs have been built.  insert into the context then move along.
38         $ctx->{$_} = $ro_object_subs->{$_} for keys %$ro_object_subs;
39         return;
40     }
41
42     # make all "field_safe" classes accesible by default in the template context
43     my @classes = grep {
44         ($Fieldmapper::fieldmap->{$_}->{field_safe} || '') =~ /true/i
45     } keys %{ $Fieldmapper::fieldmap };
46
47     for my $class (@classes) {
48
49         my $hint = $Fieldmapper::fieldmap->{$class}->{hint};
50         next if $hint eq 'aou'; # handled separately
51
52         my $ident_field =  $Fieldmapper::fieldmap->{$class}->{identity};
53         (my $eclass = $class) =~ s/Fieldmapper:://o;
54         $eclass =~ s/::/_/g;
55
56         my $list_key = "${hint}_list";
57         my $get_key = "get_$hint";
58         my $search_key = "search_$hint";
59
60         # Retrieve the full set of objects with class $hint
61         $ro_object_subs->{$list_key} = sub {
62             my $method = "retrieve_all_$eclass";
63             $cache{list}{$ctx->{locale}}{$hint} = $e->$method() unless $cache{list}{$ctx->{locale}}{$hint};
64             return $cache{list}{$ctx->{locale}}{$hint};
65         };
66
67         # locate object of class $hint with Ident field $id
68         $cache{map}{$hint} = {};
69         $ro_object_subs->{$get_key} = sub {
70             my $id = shift;
71             return $cache{map}{$ctx->{locale}}{$hint}{$id} if $cache{map}{$ctx->{locale}}{$hint}{$id};
72             ($cache{map}{$ctx->{locale}}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$ro_object_subs->{$list_key}->()};
73             return $cache{map}{$ctx->{locale}}{$hint}{$id};
74         };
75
76         # search for objects of class $hint where field=value
77         $cache{search}{$hint} = {};
78         $ro_object_subs->{$search_key} = sub {
79             my ($field, $val, $filterfield, $filterval) = @_;
80             my $method = "search_$eclass";
81             my $cacheval = $val;
82             if (ref $val) {
83                 $val = [sort(@$val)] if ref $val eq 'ARRAY';
84                 $cacheval = OpenSRF::Utils::JSON->perl2JSON($val);
85                 #$self->apache->log->info("cacheval : $cacheval");
86             }
87             my $search_obj = {$field => $val};
88             if($filterfield) {
89                 $search_obj->{$filterfield} = $filterval;
90                 $cacheval .= ':' . $filterfield . ':' . $filterval;
91             }
92             #$cache{search}{$ctx->{locale}}{$hint}{$field} = {} unless $cache{search}{$ctx->{locale}}{$hint}{$field};
93             $cache{search}{$ctx->{locale}}{$hint}{$field}{$cacheval} = $e->$method($search_obj)
94                 unless $cache{search}{$ctx->{locale}}{$hint}{$field}{$cacheval};
95             return $cache{search}{$ctx->{locale}}{$hint}{$field}{$cacheval};
96         };
97     }
98
99     $ro_object_subs->{aou_tree} = sub {
100
101         # fetch the org unit tree
102         unless($cache{aou_tree}{$ctx->{locale}}) {
103             my $tree = $e->search_actor_org_unit([
104                             {   parent_ou => undef},
105                             {   flesh            => -1,
106                                     flesh_fields    => {aou =>  ['children']},
107                                     order_by        => {aou => 'name'}
108                             }
109                     ])->[0];
110
111             # flesh the org unit type for each org unit
112             # and simultaneously set the id => aou map cache
113             sub flesh_aout {
114                 my $node = shift;
115                 my $ro_object_subs = shift;
116                 my $ctx = shift;
117                 $node->ou_type( $ro_object_subs->{get_aout}->($node->ou_type) );
118                 $cache{map}{$ctx->{locale}}{aou}{$node->id} = $node;
119                 flesh_aout($_, $ro_object_subs, $ctx) foreach @{$node->children};
120             };
121             flesh_aout($tree, $ro_object_subs, $ctx);
122
123             $cache{aou_tree}{$ctx->{locale}} = $tree;
124         }
125
126         return $cache{aou_tree}{$ctx->{locale}};
127     };
128
129     # Add a special handler for the tree-shaped org unit cache
130     $ro_object_subs->{get_aou} = sub {
131         my $org_id = shift;
132         return undef unless defined $org_id;
133         $ro_object_subs->{aou_tree}->(); # force the org tree to load
134         return $cache{map}{$ctx->{locale}}{aou}{$org_id};
135     };
136
137     # Returns a flat list of aou objects.  often easier to manage than a tree.
138     $ro_object_subs->{aou_list} = sub {
139         $ro_object_subs->{aou_tree}->(); # force the org tree to load
140         return [ values %{$cache{map}{$ctx->{locale}}{aou}} ];
141     };
142
143     $ro_object_subs->{aouct_tree} = sub {
144
145         # fetch the org unit tree
146         unless(exists $cache{aouct_tree}{$ctx->{locale}}) {
147             $cache{aouct_tree}{$ctx->{locale}} = undef;
148
149             my $tree_id = $e->search_actor_org_unit_custom_tree(
150                 {purpose => 'opac', active => 't'},
151                 {idlist => 1}
152             )->[0];
153
154             if ($tree_id) {
155                 my $node_tree = $e->search_actor_org_unit_custom_tree_node([
156                 {parent_node => undef, tree => $tree_id},
157                 {   flesh        => -1,
158                     flesh_fields => {aouctn => ['children', 'org_unit']},
159                     order_by     => {aouctn => 'sibling_order'}
160                 }
161                 ])->[0];
162
163                 # tree-ify the org units.  note that since the orgs are fleshed
164                 # upon retrieval, this org tree will not clobber ctx->{aou_tree}.
165                 my @nodes = ($node_tree);
166                 while (my $node = shift(@nodes)) {
167                     my $aou = $node->org_unit;
168                     $aou->children([]);
169                     for my $cnode (@{$node->children}) {
170                         my $child_org = $cnode->org_unit;
171                         $child_org->parent_ou($aou->id);
172                         $child_org->ou_type( $ro_object_subs->{get_aout}->($child_org->ou_type) );
173                         push(@{$aou->children}, $child_org);
174                         push(@nodes, $cnode);
175                     }
176                 }
177
178                 $cache{aouct_tree}{$ctx->{locale}} = 
179                     $node_tree->org_unit if $node_tree;
180             }
181         }
182
183         return $cache{aouct_tree}{$ctx->{locale}};
184     };
185
186     # turns an ISO date into something TT can understand
187     $ro_object_subs->{parse_datetime} = sub {
188         my $date = shift;
189
190         # Probably an accidental entry like '0212' instead of '2012',
191         # but 1) the leading 0 may get stripped in cstore and
192         # 2) DateTime::Format::ISO8601 returns an error as years
193         # must be 2 or 4 digits
194         if ($date =~ m/^\d{3}-/) {
195             $logger->warn("Invalid date had a 3-digit year: $date");
196             $date = '0' . $date;
197         } elsif ($date =~ m/^\d{1}-/) {
198             $logger->warn("Invalid date had a 1-digit year: $date");
199             $date = '000' . $date;
200         }
201
202         my $cleansed_date = cleanse_ISO8601($date);
203
204         $date = DateTime::Format::ISO8601->new->parse_datetime($cleansed_date);
205         return sprintf(
206             "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
207             $date->hour,
208             $date->minute,
209             $date->second,
210             $date->day,
211             $date->month,
212             $date->year
213         );
214     };
215
216     # retrieve and cache org unit setting values
217     $ro_object_subs->{get_org_setting} = sub {
218         my($org_id, $setting) = @_;
219
220         $cache{org_settings}{$ctx->{locale}}{$org_id}{$setting} =
221             $U->ou_ancestor_setting_value($org_id, $setting)
222                 unless exists $cache{org_settings}{$ctx->{locale}}{$org_id}{$setting};
223
224         return $cache{org_settings}{$ctx->{locale}}{$org_id}{$setting};
225     };
226
227     $ctx->{$_} = $ro_object_subs->{$_} for keys %$ro_object_subs;
228 }
229
230 sub generic_redirect {
231     my $self = shift;
232     my $url = shift;
233     my $cookie = shift; # can be an array of cgi.cookie's
234
235     $self->apache->print(
236         $self->cgi->redirect(
237             -url => $url || 
238                 $self->cgi->param('redirect_to') || 
239                 $self->ctx->{referer} || 
240                 $self->ctx->{home_page},
241             -cookie => $cookie
242         )
243     );
244
245     return Apache2::Const::REDIRECT;
246 }
247
248 my $unapi_cache;
249 sub get_records_and_facets {
250     my ($self, $rec_ids, $facet_key, $unapi_args) = @_;
251
252     $unapi_args ||= {};
253     $unapi_args->{site} ||= $self->ctx->{aou_tree}->()->shortname;
254     $unapi_args->{depth} ||= $self->ctx->{aou_tree}->()->ou_type->depth;
255     $unapi_args->{flesh_depth} ||= 5;
256
257     $unapi_cache ||= OpenSRF::Utils::Cache->new('global');
258     my $unapi_cache_key_suffix = join(
259         '_',
260         $unapi_args->{site},
261         $unapi_args->{depth},
262         $unapi_args->{flesh_depth},
263         ($unapi_args->{pref_lib} || '')
264     );
265
266     my %tmp_data;
267     my $outer_self = $self;
268     $self->timelog("get_records_and_facets(): about to call multisession");
269     my $ses = OpenSRF::MultiSession->new(
270         app => 'open-ils.cstore',
271         cap => 10, # XXX config
272         success_handler => sub {
273             my($self, $req) = @_;
274             my $data = $req->{response}->[0]->content;
275
276             $outer_self->timelog("get_records_and_facets(): got response content");
277
278             # Protect against requests for non-existent records
279             return unless $data->{'unapi.bre'};
280
281             my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'})->documentElement;
282
283             $outer_self->timelog("get_records_and_facets(): parsed xml");
284             # Protect against legacy invalid MARCXML that might not have a 901c
285             my $bre_id;
286             my $bre_id_nodes =  $xml->find('*[@tag="901"]/*[@code="c"]');
287             if ($bre_id_nodes) {
288                 $bre_id =  $bre_id_nodes->[0]->textContent;
289             } else {
290                 $logger->warn("Missing 901 subfield 'c' in " . $xml->toString());
291             }
292             $tmp_data{$bre_id} = {id => $bre_id, marc_xml => $xml};
293
294             if ($bre_id) {
295                 # Let other backends grab our data now that we're done.
296                 my $key = 'TPAC_unapi_cache_'.$bre_id.'_'.$unapi_cache_key_suffix;
297                 my $cache_data = $unapi_cache->get_cache($key);
298                 if ($$cache_data{running}) {
299                     $unapi_cache->put_cache($key, { id => $bre_id, marc_xml => $data->{'unapi.bre'} }, 10);
300                 }
301             }
302
303
304             $outer_self->timelog("get_records_and_facets(): end of success handler");
305         }
306     );
307
308     $self->timelog("get_records_and_facets(): about to call unapi.bre via json_query (rec_ids has " . scalar(@$rec_ids));
309
310     my @loop_recs = @$rec_ids;
311     my %rec_timeout;
312
313     while (my $bid = shift @loop_recs) {
314
315         sleep(0.1) if $rec_timeout{$bid};
316
317         my $unapi_cache_key = 'TPAC_unapi_cache_'.$bid.'_'.$unapi_cache_key_suffix;
318         my $unapi_data = $unapi_cache->get_cache($unapi_cache_key) || {};
319
320         if ($unapi_data->{running}) { #cache entry from ongoing, concurrent retrieval
321             if (!$rec_timeout{$bid}) {
322                 $rec_timeout{$bid} = time() + 10;
323             }
324
325             if ( time() > $rec_timeout{$bid} ) { # we've waited too long. just do it
326                 $unapi_data = {};
327                 delete $rec_timeout{$bid};
328             } else { # we'll pause next time around to let this one try again
329                 push(@loop_recs, $bid);
330                 next;
331             }
332         }
333
334         if ($unapi_data->{marc_xml}) { # we got data from the cache
335             $unapi_data->{marc_xml} = XML::LibXML->new->parse_string($unapi_data->{marc_xml})->documentElement;
336             $tmp_data{$unapi_data->{id}} = $unapi_data;
337         } else { # we're the first or we timed out. success_handler will populate the real value
338             $unapi_cache->put_cache($unapi_cache_key, { running => $$ }, 10);
339             $ses->request(
340                 'open-ils.cstore.json_query',
341                  {from => [
342                     'unapi.bre', $bid, 'marcxml','record', 
343                     $unapi_args->{flesh}, 
344                     $unapi_args->{site}, 
345                     $unapi_args->{depth}, 
346                     'acn=>' . $unapi_args->{flesh_depth} . ',acp=>' . $unapi_args->{flesh_depth}, 
347                     undef, undef, $unapi_args->{pref_lib}
348                 ]}
349             );
350         }
351
352     }
353
354
355     $self->timelog("get_records_and_facets():almost ready to fetch facets");
356     # collect the facet data
357     my $search = OpenSRF::AppSession->create('open-ils.search');
358     my $facet_req = $search->request(
359         'open-ils.search.facet_cache.retrieve', $facet_key
360     ) if $facet_key;
361
362     # gather up the unapi recs
363     $ses->session_wait(1);
364     $self->timelog("get_records_and_facets():past session wait");
365
366     my $facets = {};
367     if ($facet_key) {
368         my $tmp_facets = $facet_req->gather(1);
369         $self->timelog("get_records_and_facets(): gathered facet data");
370         for my $cmf_id (keys %$tmp_facets) {
371
372             # sort highest to lowest match count
373             my @entries;
374             my $entries = $tmp_facets->{$cmf_id};
375             for my $ent (keys %$entries) {
376                 push(@entries, {value => $ent, count => $$entries{$ent}});
377             };
378
379             # Sort facet entries by 1) count descending, 2) text ascending
380             @entries = sort {
381                 $b->{count} <=> $a->{count} ||
382                 $a->{value} cmp $b->{value}
383             } @entries;
384
385             $facets->{$cmf_id} = {
386                 cmf => $self->ctx->{get_cmf}->($cmf_id),
387                 data => \@entries
388             }
389         }
390         $self->timelog("get_records_and_facets(): gathered/sorted facet data");
391     } else {
392         $facets = undef;
393     }
394
395     $search->kill_me;
396
397     return ($facets, map { $tmp_data{$_} } @$rec_ids);
398 }
399
400 # TODO: blend this code w/ ^-- get_records_and_facets
401 sub fetch_marc_xml_by_id {
402     my ($self, $id_list) = @_;
403     $id_list = [$id_list] unless ref($id_list);
404
405     {
406         no warnings qw/numeric/;
407         $id_list = [map { int $_ } @$id_list];
408         $id_list = [grep { $_ > 0} @$id_list];
409     };
410
411     return {} if scalar(@$id_list) < 1;
412
413     # I'm just sure there needs to be some more efficient way to get all of
414     # this.
415     my $results = $self->editor->json_query({
416         "select" => {"bre" => ["id", "marc"]},
417         "from" => {"bre" => {}},
418         "where" => {"id" => $id_list}
419     }, {substream => 1}) or return $self->editor->die_event;
420
421     my $marc_xml = {};
422     for my $r (@$results) {
423         $marc_xml->{$r->{"id"}} =
424             (new XML::LibXML)->parse_string($r->{"marc"});
425     }
426
427     return $marc_xml;
428 }
429
430 sub _get_search_lib {
431     my $self = shift;
432     my $ctx = $self->ctx;
433
434     # avoid duplicate lookups
435     return $ctx->{search_ou} if $ctx->{search_ou};
436
437     my $loc = $ctx->{copy_location_group_org};
438     return $loc if $loc;
439
440     # loc param takes precedence
441     $loc = $self->cgi->param('loc');
442     return $loc if $loc;
443
444     my $pref_lib = $self->_get_pref_lib();
445     return $pref_lib if $pref_lib;
446
447     return $ctx->{aou_tree}->()->id;
448 }
449
450 sub _get_pref_lib {
451     my $self = shift;
452     my $ctx = $self->ctx;
453
454     # plib param takes precedence
455     my $plib = $self->cgi->param('plib');
456     return $plib if $plib;
457
458     if ($ctx->{user}) {
459         # See if the user has a search library preference
460         my $lset = $self->editor->search_actor_user_setting({
461             usr => $ctx->{user}->id, 
462             name => 'opac.default_search_location'
463         })->[0];
464         return OpenSRF::Utils::JSON->JSON2perl($lset->value) if $lset;
465
466         # Otherwise return the user's home library
467         return $ctx->{user}->home_ou;
468     }
469
470     if ($ctx->{physical_loc}) {
471         return $ctx->{physical_loc};
472     }
473
474 }
475
476 # This is defensively coded since we don't do much manual reading from the
477 # file system in this module.
478 sub load_eg_cache_hash {
479     my ($self) = @_;
480
481     # just a context helper
482     $self->ctx->{eg_cache_hash} = sub { return $cache{eg_cache_hash}; };
483
484     # Need to actually load the value? If already done, move on.
485     return if defined $cache{eg_cache_hash};
486
487     # In this way even if we fail, we won't slow things down by ever trying
488     # again within this Apache process' lifetime.
489     $cache{eg_cache_hash} = 0;
490
491     my $path = File::Spec->catfile(
492         $self->apache->document_root, "eg_cache_hash"
493     );
494
495     if (not open FH, "<$path") {
496         $self->apache->log->warn("error opening $path : $!");
497         return;
498     } else {
499         my $buf;
500         my $rv = read FH, $buf, 64;  # defensive
501         close FH;
502
503         if (not defined $rv) {  # error
504             $self->apache->log->warn("error reading $path : $!");
505         } elsif ($rv > 0) {     # no error, something read
506             chomp $buf;
507             $cache{eg_cache_hash} = $buf;
508         }
509     }
510 }
511
512 # Extracts the copy location org unit and group from the 
513 # "logc" param, which takes the form org_id:grp_id.
514 sub extract_copy_location_group_info {
515     my $self = shift;
516     my $ctx = $self->ctx;
517     if (my $clump = $self->cgi->param('locg')) {
518         my ($org, $grp) = split(/:/, $clump);
519         $ctx->{copy_location_group_org} = $org;
520         $ctx->{copy_location_group} = $grp if $grp;
521     }
522 }
523
524 sub load_copy_location_groups {
525     my $self = shift;
526     my $ctx = $self->ctx;
527
528     # User can access to the search location groups at the current 
529     # search lib, the physical location lib, and the patron's home ou.
530     my @ctx_orgs = $ctx->{search_ou};
531     push(@ctx_orgs, $ctx->{physical_loc}) if $ctx->{physical_loc};
532     push(@ctx_orgs, $ctx->{user}->home_ou) if $ctx->{user};
533
534     my $grps = $self->editor->search_asset_copy_location_group([
535         {
536             opac_visible => 't',
537             owner => {
538                 in => {
539                     select => {aou => [{
540                         column => 'id', 
541                         transform => 'actor.org_unit_full_path',
542                         result_field => 'id',
543                     }]},
544                     from => 'aou',
545                     where => {id => \@ctx_orgs}
546                 }
547             }
548         },
549         {order_by => {acplg => 'pos'}}
550     ]);
551
552     my %buckets;
553     push(@{$buckets{$_->owner}}, $_) for @$grps;
554     $ctx->{copy_location_groups} = \%buckets;
555 }
556
557 sub set_file_download_headers {
558     my $self = shift;
559     my $filename = shift;
560     my $ctype = shift || "text/plain; encoding=utf8";
561
562     $self->apache->content_type($ctype);
563
564     $self->apache->headers_out->add(
565         "Content-Disposition",
566         "attachment;filename=$filename"
567     );
568
569     return Apache2::Const::OK;
570 }
571
572 sub apache_log_if_event {
573     my ($self, $event, $prefix_text, $success_ok, $level) = @_;
574
575     $prefix_text ||= "Evergreen returned event";
576     $success_ok ||= 0;
577     $level ||= "warn";
578
579     chomp $prefix_text;
580     $prefix_text .= ": ";
581
582     my $code = $U->event_code($event);
583     if (defined $code and ($code or not $success_ok)) {
584         $self->apache->log->$level(
585             $prefix_text .
586             ($event->{textcode} || "") . " ($code)" .
587             ($event->{note} ? (": " . $event->{note}) : "")
588         );
589         return 1;
590     }
591
592     return;
593 }
594
595 sub load_search_filter_groups {
596     my $self = shift;
597     my $ctx_org = shift;
598     my $org_list = $U->get_org_ancestors($ctx_org, 1);
599
600     my %seen;
601     for my $org_id (@$org_list) {
602
603         my $grps;
604         if (! ($grps = $cache{search_filter_groups}{$org_id}) ) {
605             $grps = $self->editor->search_actor_search_filter_group([
606                 {owner => $org_id},
607                 {   flesh => 2, 
608                     flesh_fields => {
609                         asfg => ['entries'],
610                         asfge => ['query']
611                     },
612                     order_by => {asfge => 'pos'}
613                 }
614             ]);
615             $cache{search_filter_groups}{$org_id} = $grps;
616         }
617
618         # for the current context, if a descendant org has a group 
619         # with a matching code replace the group from the parent.
620         $seen{$_->code} = $_ for @$grps;
621     }
622
623     return $self->ctx->{search_filter_groups} = \%seen;
624 }
625
626
627 sub check_for_temp_list_warning {
628     my $self = shift;
629     my $ctx = $self->ctx;
630     my $cgi = $self->cgi;
631
632     my $lib = $self->_get_search_lib;
633     my $warn = ($ctx->{get_org_setting}->($lib || 1, 'opac.patron.temporary_list_warn')) ? 1 : 0;
634
635     if ($warn && $ctx->{user}) {
636         $self->_load_user_with_prefs;
637         my $map = $ctx->{user_setting_map};
638         $warn = 0 if ($$map{'opac.temporary_list_no_warn'});
639     }
640
641     # Check for a cookie disabling the warning.
642     $warn = 0 if ($warn && $cgi->cookie('no_temp_list_warn'));
643
644     return $warn;
645 }
646
647 1;