]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
96ce5da3ee20225ffbe9b86f5cf7e702259024ee
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Search / Biblio.pm
1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
4
5
6 use OpenSRF::Utils::JSON;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::ModsParser;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenSRF::Utils::Cache;
12 use Encode;
13
14 use OpenSRF::Utils::Logger qw/:logger/;
15
16
17 use OpenSRF::Utils::JSON;
18
19 use Time::HiRes qw(time sleep);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
22
23 use XML::LibXML;
24 use XML::LibXSLT;
25
26 use Data::Dumper;
27 $Data::Dumper::Indent = 0;
28
29 use OpenILS::Const qw/:const/;
30
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
33 my $U = $apputils;
34
35 my $pfx = "open-ils.search_";
36
37 my $cache;
38 my $cache_timeout;
39 my $superpage_size;
40 my $max_superpages;
41
42 sub initialize {
43     $cache = OpenSRF::Utils::Cache->new('global');
44     my $sclient = OpenSRF::Utils::SettingsClient->new();
45     $cache_timeout = $sclient->config_value(
46             "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
47
48     $superpage_size = $sclient->config_value(
49             "apps", "open-ils.search", "app_settings", "superpage_size" ) || 500;
50
51     $max_superpages = $sclient->config_value(
52             "apps", "open-ils.search", "app_settings", "max_superpages" ) || 20;
53
54     $logger->info("Search cache timeout is $cache_timeout, ".
55         " superpage_size is $superpage_size, max_superpages is $max_superpages");
56 }
57
58
59
60 # ---------------------------------------------------------------------------
61 # takes a list of record id's and turns the docs into friendly 
62 # mods structures. Creates one MODS structure for each doc id.
63 # ---------------------------------------------------------------------------
64 sub _records_to_mods {
65     my @ids = @_;
66     
67     my @results;
68     my @marcxml_objs;
69
70     my $session = OpenSRF::AppSession->create("open-ils.cstore");
71     my $request = $session->request(
72             "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
73
74     while( my $resp = $request->recv ) {
75         my $content = $resp->content;
76         next if $content->id == OILS_PRECAT_RECORD;
77         my $u = OpenILS::Utils::ModsParser->new();  # FIXME: we really need a new parser for each object?
78         $u->start_mods_batch( $content->marc );
79         my $mods = $u->finish_mods_batch();
80         $mods->doc_id($content->id());
81         $mods->tcn($content->tcn_value);
82         push @results, $mods;
83     }
84
85     $session->disconnect();
86     return \@results;
87 }
88
89 __PACKAGE__->register_method(
90     method    => "record_id_to_mods",
91     api_name  => "open-ils.search.biblio.record.mods.retrieve",
92     argc      => 1,
93     signature => {
94         desc   => "Provide ID, we provide the MODS object with copy count.  " 
95                 . "Note: this method does NOT take an array of IDs like mods_slim.retrieve",    # FIXME: do it here too
96         params => [
97             { desc => 'Record ID', type => 'number' }
98         ],
99         return => {
100             desc => 'MODS object', type => 'object'
101         }
102     }
103 );
104
105 # converts a record into a mods object with copy counts attached
106 sub record_id_to_mods {
107
108     my( $self, $client, $org_id, $id ) = @_;
109
110     my $mods_list = _records_to_mods( $id );
111     my $mods_obj  = $mods_list->[0];
112     my $cmethod   = $self->method_lookup("open-ils.search.biblio.record.copy_count");
113     my ($count)   = $cmethod->run($org_id, $id);
114     $mods_obj->copy_count($count);
115
116     return $mods_obj;
117 }
118
119
120
121 __PACKAGE__->register_method(
122     method        => "record_id_to_mods_slim",
123     api_name      => "open-ils.search.biblio.record.mods_slim.retrieve",
124     argc          => 1,
125     authoritative => 1,
126     signature     => {
127         desc   => "Provide ID(s), we provide the MODS",
128         params => [
129             { desc => 'Record ID or array of IDs' }
130         ],
131         return => {
132             desc => 'MODS object(s), event on error'
133         }
134     }
135 );
136
137 # converts a record into a mods object with NO copy counts attached
138 sub record_id_to_mods_slim {
139     my( $self, $client, $id ) = @_;
140     return undef unless defined $id;
141
142     if(ref($id) and ref($id) == 'ARRAY') {
143         return _records_to_mods( @$id );
144     }
145     my $mods_list = _records_to_mods( $id );
146     my $mods_obj  = $mods_list->[0];
147     return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
148     return $mods_obj;
149 }
150
151
152
153 __PACKAGE__->register_method(
154     method   => "record_id_to_mods_slim_batch",
155     api_name => "open-ils.search.biblio.record.mods_slim.batch.retrieve",
156     stream   => 1
157 );
158 sub record_id_to_mods_slim_batch {
159     my($self, $conn, $id_list) = @_;
160     $conn->respond(_records_to_mods($_)->[0]) for @$id_list;
161     return undef;
162 }
163
164
165 # Returns the number of copies attached to a record based on org location
166 __PACKAGE__->register_method(
167     method   => "record_id_to_copy_count",
168     api_name => "open-ils.search.biblio.record.copy_count",
169     signature => {
170         desc => q/Returns a copy summary for the given record for the context org
171             unit and all ancestor org units/,
172         params => [
173             {desc => 'Context org unit id', type => 'number'},
174             {desc => 'Record ID', type => 'number'}
175         ],
176         return => {
177             desc => q/summary object per org unit in the set, where the set
178                 includes the context org unit and all parent org units.  
179                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
180                 "unshadow", "available".  Each is a count, except "org_unit" which is 
181                 the context org unit and "depth" which is the depth of the context org unit
182             /,
183             type => 'array'
184         }
185     }
186 );
187
188 __PACKAGE__->register_method(
189     method        => "record_id_to_copy_count",
190     api_name      => "open-ils.search.biblio.record.copy_count.staff",
191     authoritative => 1,
192     signature => {
193         desc => q/Returns a copy summary for the given record for the context org
194             unit and all ancestor org units/,
195         params => [
196             {desc => 'Context org unit id', type => 'number'},
197             {desc => 'Record ID', type => 'number'}
198         ],
199         return => {
200             desc => q/summary object per org unit in the set, where the set
201                 includes the context org unit and all parent org units.  
202                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
203                 "unshadow", "available".  Each is a count, except "org_unit" which is 
204                 the context org unit and "depth" which is the depth of the context org unit
205             /,
206             type => 'array'
207         }
208     }
209 );
210
211 __PACKAGE__->register_method(
212     method   => "record_id_to_copy_count",
213     api_name => "open-ils.search.biblio.metarecord.copy_count",
214     signature => {
215         desc => q/Returns a copy summary for the given record for the context org
216             unit and all ancestor org units/,
217         params => [
218             {desc => 'Context org unit id', type => 'number'},
219             {desc => 'Record ID', type => 'number'}
220         ],
221         return => {
222             desc => q/summary object per org unit in the set, where the set
223                 includes the context org unit and all parent org units.  
224                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
225                 "unshadow", "available".  Each is a count, except "org_unit" which is 
226                 the context org unit and "depth" which is the depth of the context org unit
227             /,
228             type => 'array'
229         }
230     }
231 );
232
233 __PACKAGE__->register_method(
234     method   => "record_id_to_copy_count",
235     api_name => "open-ils.search.biblio.metarecord.copy_count.staff",
236     signature => {
237         desc => q/Returns a copy summary for the given record for the context org
238             unit and all ancestor org units/,
239         params => [
240             {desc => 'Context org unit id', type => 'number'},
241             {desc => 'Record ID', type => 'number'}
242         ],
243         return => {
244             desc => q/summary object per org unit in the set, where the set
245                 includes the context org unit and all parent org units.  
246                 Object includes the keys "transcendant", "count", "org_unit", "depth", 
247                 "unshadow", "available".  Each is a count, except "org_unit" which is 
248                 the context org unit and "depth" which is the depth of the context org
249                 unit.  "depth" is always -1 when the count from a lasso search is
250                 performed, since depth doesn't mean anything in a lasso context.
251             /,
252             type => 'array'
253         }
254     }
255 );
256
257 sub record_id_to_copy_count {
258     my( $self, $client, $org_id, $record_id ) = @_;
259
260     return [] unless $record_id;
261
262     my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
263     my $staff = $self->api_name =~ /staff/ ? 't' : 'f';
264
265     my $data = $U->cstorereq(
266         "open-ils.cstore.json_query.atomic",
267         { from => ['asset.' . $key  . '_copy_count' => $org_id => $record_id => $staff] }
268     );
269
270     my @count;
271     for my $d ( @$data ) { # fix up the key name change required by stored-proc version
272         $$d{count} = delete $$d{visible};
273         push @count, $d;
274     }
275
276     return [ sort { $a->{depth} <=> $b->{depth} } @count ];
277 }
278
279 __PACKAGE__->register_method(
280     method   => "record_has_holdable_copy",
281     api_name => "open-ils.search.biblio.record.has_holdable_copy",
282     signature => {
283         desc => q/Returns a boolean indicating if a record has any holdable copies./,
284         params => [
285             {desc => 'Record ID', type => 'number'}
286         ],
287         return => {
288             desc => q/bool indicating if the record has any holdable copies/,
289             type => 'bool'
290         }
291     }
292 );
293
294 __PACKAGE__->register_method(
295     method   => "record_has_holdable_copy",
296     api_name => "open-ils.search.biblio.metarecord.has_holdable_copy",
297     signature => {
298         desc => q/Returns a boolean indicating if a record has any holdable copies./,
299         params => [
300             {desc => 'Record ID', type => 'number'}
301         ],
302         return => {
303             desc => q/bool indicating if the record has any holdable copies/,
304             type => 'bool'
305         }
306     }
307 );
308
309 sub record_has_holdable_copy {
310     my($self, $client, $record_id ) = @_;
311
312     return 0 unless $record_id;
313
314     my $key = $self->api_name =~ /metarecord/ ? 'metarecord' : 'record';
315
316     my $data = $U->cstorereq(
317         "open-ils.cstore.json_query.atomic",
318         { from => ['asset.' . $key . '_has_holdable_copy' => $record_id ] }
319     );
320
321     return ${@$data[0]}{'asset.' . $key . '_has_holdable_copy'} eq 't';
322
323 }
324
325 __PACKAGE__->register_method(
326     method   => "biblio_search_tcn",
327     api_name => "open-ils.search.biblio.tcn",
328     argc     => 1,
329     signature => {
330         desc   => "Retrieve related record ID(s) given a TCN",
331         params => [
332             { desc => 'TCN', type => 'string' },
333             { desc => 'Flag indicating to include deleted records', type => 'string' }
334         ],
335         return => {
336             desc => 'Results object like: { "count": $i, "ids": [...] }',
337             type => 'object'
338         }
339     }
340
341 );
342
343 sub biblio_search_tcn {
344
345     my( $self, $client, $tcn, $include_deleted ) = @_;
346
347     $tcn =~ s/^\s+|\s+$//og;
348
349     my $e = new_editor();
350     my $search = {tcn_value => $tcn};
351     $search->{deleted} = 'f' unless $include_deleted;
352     my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
353     
354     return { count => scalar(@$recs), ids => $recs };
355 }
356
357
358 # --------------------------------------------------------------------------------
359
360 __PACKAGE__->register_method(
361     method   => "biblio_barcode_to_copy",
362     api_name => "open-ils.search.asset.copy.find_by_barcode",
363 );
364 sub biblio_barcode_to_copy { 
365     my( $self, $client, $barcode ) = @_;
366     my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
367     return $evt if $evt;
368     return $copy;
369 }
370
371 __PACKAGE__->register_method(
372     method   => "biblio_id_to_copy",
373     api_name => "open-ils.search.asset.copy.batch.retrieve",
374 );
375 sub biblio_id_to_copy { 
376     my( $self, $client, $ids ) = @_;
377     $logger->info("Fetching copies @$ids");
378     return $U->cstorereq(
379         "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
380 }
381
382
383 __PACKAGE__->register_method(
384     method  => "biblio_id_to_uris",
385     api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
386     argc    => 2, 
387     stream  => 1,
388     signature => q#
389         @param BibID Which bib record contains the URIs
390         @param OrgID Where to look for URIs
391         @param OrgDepth Range adjustment for OrgID
392         @return A stream or list of 'auri' objects
393     #
394
395 );
396 sub biblio_id_to_uris { 
397     my( $self, $client, $bib, $org, $depth ) = @_;
398     die "Org ID required" unless defined($org);
399     die "Bib ID required" unless defined($bib);
400
401     my @params;
402     push @params, $depth if (defined $depth);
403
404     my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
405         {   select  => { auri => [ 'id' ] },
406             from    => {
407                 acn => {
408                     auricnm => {
409                         field   => 'call_number',
410                         fkey    => 'id',
411                         join    => {
412                             auri    => {
413                                 field => 'id',
414                                 fkey => 'uri',
415                                 filter  => { active => 't' }
416                             }
417                         }
418                     }
419                 }
420             },
421             where   => {
422                 '+acn'  => {
423                     record      => $bib,
424                     owning_lib  => {
425                         in  => {
426                             select  => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
427                             from    => 'aou',
428                             where   => { id => $org },
429                             distinct=> 1
430                         }
431                     }
432                 }
433             },
434             distinct=> 1,
435         }
436     );
437
438     my $uris = $U->cstorereq(
439         "open-ils.cstore.direct.asset.uri.search.atomic",
440         { id => [ map { (values %$_) } @$ids ] }
441     );
442
443     $client->respond($_) for (@$uris);
444
445     return undef;
446 }
447
448
449 __PACKAGE__->register_method(
450     method    => "copy_retrieve",
451     api_name  => "open-ils.search.asset.copy.retrieve",
452     argc      => 1,
453     signature => {
454         desc   => 'Retrieve a copy object based on the Copy ID',
455         params => [
456             { desc => 'Copy ID', type => 'number'}
457         ],
458         return => {
459             desc => 'Copy object, event on error'
460         }
461     }
462 );
463
464 sub copy_retrieve {
465     my( $self, $client, $cid ) = @_;
466     my( $copy, $evt ) = $U->fetch_copy($cid);
467     return $evt || $copy;
468 }
469
470 __PACKAGE__->register_method(
471     method   => "volume_retrieve",
472     api_name => "open-ils.search.asset.call_number.retrieve"
473 );
474 sub volume_retrieve {
475     my( $self, $client, $vid ) = @_;
476     my $e = new_editor();
477     my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
478     return $vol;
479 }
480
481 __PACKAGE__->register_method(
482     method        => "fleshed_copy_retrieve_batch",
483     api_name      => "open-ils.search.asset.copy.fleshed.batch.retrieve",
484     authoritative => 1,
485 );
486
487 sub fleshed_copy_retrieve_batch { 
488     my( $self, $client, $ids ) = @_;
489     $logger->info("Fetching fleshed copies @$ids");
490     return $U->cstorereq(
491         "open-ils.cstore.direct.asset.copy.search.atomic",
492         { id => $ids },
493         { flesh => 1, 
494           flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries parts / ] }
495         });
496 }
497
498
499 __PACKAGE__->register_method(
500     method   => "fleshed_copy_retrieve",
501     api_name => "open-ils.search.asset.copy.fleshed.retrieve",
502 );
503
504 sub fleshed_copy_retrieve { 
505     my( $self, $client, $id ) = @_;
506     my( $c, $e) = $U->fetch_fleshed_copy($id);
507     return $e || $c;
508 }
509
510
511 __PACKAGE__->register_method(
512     method        => 'fleshed_by_barcode',
513     api_name      => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
514     authoritative => 1,
515 );
516 sub fleshed_by_barcode {
517     my( $self, $conn, $barcode ) = @_;
518     my $e = new_editor();
519     my $copyid = $e->search_asset_copy(
520         {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
521         or return $e->event;
522     return fleshed_copy_retrieve2( $self, $conn, $copyid);
523 }
524
525
526 __PACKAGE__->register_method(
527     method        => "fleshed_copy_retrieve2",
528     api_name      => "open-ils.search.asset.copy.fleshed2.retrieve",
529     authoritative => 1,
530 );
531
532 sub fleshed_copy_retrieve2 { 
533     my( $self, $client, $id ) = @_;
534     my $e = new_editor();
535     my $copy = $e->retrieve_asset_copy(
536         [
537             $id,
538             {
539                 flesh        => 2,
540                 flesh_fields => {
541                     acp => [
542                         qw/ location status stat_cat_entry_copy_maps notes age_protect parts peer_record_maps /
543                     ],
544                     ascecm => [qw/ stat_cat stat_cat_entry /],
545                 }
546             }
547         ]
548     ) or return $e->event;
549
550     # For backwards compatibility
551     #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
552
553     if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
554         $copy->circulations(
555             $e->search_action_circulation( 
556                 [   
557                     { target_copy => $copy->id },
558                     {
559                         order_by => { circ => 'xact_start desc' },
560                         limit => 1
561                     }
562                 ]
563             )
564         );
565     }
566
567     return $copy;
568 }
569
570
571 __PACKAGE__->register_method(
572     method        => 'flesh_copy_custom',
573     api_name      => 'open-ils.search.asset.copy.fleshed.custom',
574     authoritative => 1,
575 );
576
577 sub flesh_copy_custom {
578     my( $self, $conn, $copyid, $fields ) = @_;
579     my $e = new_editor();
580     my $copy = $e->retrieve_asset_copy(
581         [
582             $copyid,
583             { 
584                 flesh               => 1,
585                 flesh_fields    => { 
586                     acp => $fields,
587                 }
588             }
589         ]
590     ) or return $e->event;
591     return $copy;
592 }
593
594
595 __PACKAGE__->register_method(
596     method   => "biblio_barcode_to_title",
597     api_name => "open-ils.search.biblio.find_by_barcode",
598 );
599
600 sub biblio_barcode_to_title {
601     my( $self, $client, $barcode ) = @_;
602
603     my $title = $apputils->simple_scalar_request(
604         "open-ils.storage",
605         "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
606
607     return { ids => [ $title->id ], count => 1 } if $title;
608     return { count => 0 };
609 }
610
611 __PACKAGE__->register_method(
612     method        => 'title_id_by_item_barcode',
613     api_name      => 'open-ils.search.bib_id.by_barcode',
614     authoritative => 1,
615     signature => { 
616         desc   => 'Retrieve bib record id associated with the copy identified by the given barcode',
617         params => [
618             { desc => 'Item barcode', type => 'string' }
619         ],
620         return => {
621             desc => 'Bib record id.'
622         }
623     }
624 );
625
626 __PACKAGE__->register_method(
627     method        => 'title_id_by_item_barcode',
628     api_name      => 'open-ils.search.multi_home.bib_ids.by_barcode',
629     authoritative => 1,
630     signature => {
631         desc   => 'Retrieve bib record ids associated with the copy identified by the given barcode.  This includes peer bibs for Multi-Home items.',
632         params => [
633             { desc => 'Item barcode', type => 'string' }
634         ],
635         return => {
636             desc => 'Array of bib record ids.  First element is the native bib for the item.'
637         }
638     }
639 );
640
641
642 sub title_id_by_item_barcode {
643     my( $self, $conn, $barcode ) = @_;
644     my $e = new_editor();
645     my $copies = $e->search_asset_copy(
646         [
647             { deleted => 'f', barcode => $barcode },
648             {
649                 flesh => 2,
650                 flesh_fields => {
651                     acp => [ 'call_number' ],
652                     acn => [ 'record' ]
653                 }
654             }
655         ]
656     );
657
658     return $e->event unless @$copies;
659
660     if( $self->api_name =~ /multi_home/ ) {
661         my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
662             [
663                 { target_copy => $$copies[0]->id }
664             ]
665         );
666         my @temp =  map { $_->peer_record } @{ $multi_home_list };
667         unshift @temp, $$copies[0]->call_number->record->id;
668         return \@temp;
669     } else {
670         return $$copies[0]->call_number->record->id;
671     }
672 }
673
674 __PACKAGE__->register_method(
675     method        => 'find_peer_bibs',
676     api_name      => 'open-ils.search.peer_bibs.test',
677     authoritative => 1,
678     signature => {
679         desc   => 'Tests to see if the specified record is a peer record.',
680         params => [
681             { desc => 'Biblio record entry Id', type => 'number' }
682         ],
683         return => {
684             desc => 'True if specified id can be found in biblio.peer_bib_copy_map.peer_record.',
685             type => 'bool'
686         }
687     }
688 );
689
690 __PACKAGE__->register_method(
691     method        => 'find_peer_bibs',
692     api_name      => 'open-ils.search.peer_bibs',
693     authoritative => 1,
694     signature => {
695         desc   => 'Return acps and mvrs for multi-home items linked to specified peer record.',
696         params => [
697             { desc => 'Biblio record entry Id', type => 'number' }
698         ],
699         return => {
700             desc => '{ records => Array of mvrs, items => array of acps }',
701         }
702     }
703 );
704
705
706 sub find_peer_bibs {
707     my( $self, $client, $doc_id ) = @_;
708     my $e = new_editor();
709
710     my $multi_home_list = $e->search_biblio_peer_bib_copy_map(
711         [
712             { peer_record => $doc_id },
713             {
714                 flesh => 2,
715                 flesh_fields => {
716                     bpbcm => [ 'target_copy', 'peer_type' ],
717                     acp => [ 'call_number', 'location', 'status', 'peer_record_maps' ]
718                 }
719             }
720         ]
721     );
722
723     if ($self->api_name =~ /test/) {
724         return scalar( @{$multi_home_list} ) > 0 ? 1 : 0;
725     }
726
727     if (scalar(@{$multi_home_list})==0) {
728         return [];
729     }
730
731     # create a unique hash of the primary record MVRs for foreign copies
732     # XXX PLEASE let's change to unAPI2 (supports foreign copies) in the TT opac?!?
733     my %rec_hash = map {
734         ($_->target_copy->call_number->record, _records_to_mods( $_->target_copy->call_number->record )->[0])
735     } @$multi_home_list;
736
737     # set the foreign_copy_maps field to an empty array
738     map { $rec_hash{$_}->foreign_copy_maps([]) } keys( %rec_hash );
739
740     # push the maps onto the correct MVRs
741     for (@$multi_home_list) {
742         push(
743             @{$rec_hash{ $_->target_copy->call_number->record }->foreign_copy_maps()},
744             $_
745         );
746     }
747
748     return [sort {$a->title cmp $b->title} values(%rec_hash)];
749 };
750
751 __PACKAGE__->register_method(
752     method   => "biblio_copy_to_mods",
753     api_name => "open-ils.search.biblio.copy.mods.retrieve",
754 );
755
756 # takes a copy object and returns it fleshed mods object
757 sub biblio_copy_to_mods {
758     my( $self, $client, $copy ) = @_;
759
760     my $volume = $U->cstorereq( 
761         "open-ils.cstore.direct.asset.call_number.retrieve",
762         $copy->call_number() );
763
764     my $mods = _records_to_mods($volume->record());
765     $mods = shift @$mods;
766     $volume->copies([$copy]);
767     push @{$mods->call_numbers()}, $volume;
768
769     return $mods;
770 }
771
772
773 =head1 NAME
774
775 OpenILS::Application::Search::Biblio
776
777 =head1 DESCRIPTION
778
779 =head2 API METHODS
780
781 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
782
783 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
784
785 The query argument is a string, but built like a hash with key: value pairs.
786 Recognized search keys include: 
787
788  keyword (kw) - search keyword(s) *
789  author  (au) - search author(s)  *
790  name    (au) - same as author    *
791  title   (ti) - search title      *
792  subject (su) - search subject    *
793  series  (se) - search series     *
794  lang - limit by language (specify multiple langs with lang:l1 lang:l2 ...)
795  site - search at specified org unit, corresponds to actor.org_unit.shortname
796  pref_ou - extend search to specified org unit, corresponds to actor.org_unit.shortname
797  sort - sort type (title, author, pubdate)
798  dir  - sort direction (asc, desc)
799  available - if set to anything other than "false" or "0", limits to available items
800
801 * Searching keyword, author, title, subject, and series supports additional search 
802 subclasses, specified with a "|".  For example, C<title|proper:gone with the wind>.
803
804 For more, see B<config.metabib_field>.
805
806 =cut
807
808 foreach (qw/open-ils.search.biblio.multiclass.query
809             open-ils.search.biblio.multiclass.query.staff
810             open-ils.search.metabib.multiclass.query
811             open-ils.search.metabib.multiclass.query.staff/)
812 {
813 __PACKAGE__->register_method(
814     api_name  => $_,
815     method    => 'multiclass_query',
816     signature => {
817         desc   => 'Perform a search query.  The .staff version of the call includes otherwise hidden hits.',
818         params => [
819             {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)',         type => 'object'},
820             {name => 'query',   desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
821             {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
822         ],
823         return => {
824             desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
825             type => 'object',       # TODO: update as miker's new elements are included
826         }
827     }
828 );
829 }
830
831 sub multiclass_query {
832     # arghash only really supports limit/offset anymore
833     my($self, $conn, $arghash, $query, $docache) = @_;
834
835     if ($query) {
836         $query =~ s/\+/ /go;
837         $query =~ s/^\s+//go;
838         $query =~ s/\s+/ /go;
839         $arghash->{query} = $query
840     }
841
842     $logger->debug("initial search query => $query") if $query;
843
844     (my $method = $self->api_name) =~ s/\.query/.staged/o;
845     return $self->method_lookup($method)->dispatch($arghash, $docache);
846
847 }
848
849 __PACKAGE__->register_method(
850     method    => 'cat_search_z_style_wrapper',
851     api_name  => 'open-ils.search.biblio.zstyle',
852     stream    => 1,
853     signature => q/@see open-ils.search.biblio.multiclass/
854 );
855
856 __PACKAGE__->register_method(
857     method    => 'cat_search_z_style_wrapper',
858     api_name  => 'open-ils.search.biblio.zstyle.staff',
859     stream    => 1,
860     signature => q/@see open-ils.search.biblio.multiclass/
861 );
862
863 sub cat_search_z_style_wrapper {
864     my $self = shift;
865     my $client = shift;
866     my $authtoken = shift;
867     my $args = shift;
868
869     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
870
871     my $ou = $cstore->request(
872         'open-ils.cstore.direct.actor.org_unit.search',
873         { parent_ou => undef }
874     )->gather(1);
875
876     my $result = { service => 'native-evergreen-catalog', records => [] };
877     my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
878
879     $$searchhash{searches}{title}{term}   = $$args{search}{title}   if $$args{search}{title};
880     $$searchhash{searches}{author}{term}  = $$args{search}{author}  if $$args{search}{author};
881     $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
882     $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
883     $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
884     $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
885     $$searchhash{searches}{'identifier|upc'}{term} = $$args{search}{upc} if $$args{search}{upc};
886
887     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn}       if $$args{search}{tcn};
888     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
889     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate}   if $$args{search}{pubdate};
890     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
891
892     my $method = 'open-ils.search.biblio.multiclass.staged';
893     $method .= '.staff' if $self->api_name =~ /staff$/;
894
895     my ($list) = $self->method_lookup($method)->run( $searchhash );
896
897     if ($list->{count} > 0 and @{$list->{ids}}) {
898         $result->{count} = $list->{count};
899
900         my $records = $cstore->request(
901             'open-ils.cstore.direct.biblio.record_entry.search.atomic',
902             { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
903         )->gather(1);
904
905         for my $rec ( @$records ) {
906             
907             my $u = OpenILS::Utils::ModsParser->new();
908                         $u->start_mods_batch( $rec->marc );
909                         my $mods = $u->finish_mods_batch();
910
911             push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
912
913         }
914
915     }
916
917     $cstore->disconnect();
918     return $result;
919 }
920
921 # ----------------------------------------------------------------------------
922 # These are the main OPAC search methods
923 # ----------------------------------------------------------------------------
924
925 __PACKAGE__->register_method(
926     method    => 'the_quest_for_knowledge',
927     api_name  => 'open-ils.search.biblio.multiclass',
928     signature => {
929         desc => "Performs a multi class biblio or metabib search",
930         params => [
931             {
932                 desc => "A search hash with keys: "
933                       . "searches, org_unit, depth, limit, offset, format, sort, sort_dir.  "
934                       . "See perldoc " . __PACKAGE__ . " for more detail",
935                 type => 'object',
936             },
937             {
938                 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
939                 type => 'string',
940             }
941         ],
942         return => {
943             desc => 'An object of the form: '
944                   . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
945         }
946     }
947 );
948
949 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
950
951 The search-hash argument can have the following elements:
952
953     searches: { "$class" : "$value", ...}           [REQUIRED]
954     org_unit: The org id to focus the search at
955     depth   : The org depth     
956     limit   : The search limit      default: 10
957     offset  : The search offset     default:  0
958     format  : The MARC format
959     sort    : What field to sort the results on? [ author | title | pubdate ]
960     sort_dir: What direction do we sort? [ asc | desc ]
961     tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
962         will be tagged with an additional value ("1") as the last value in the record ID array for
963         each record.  Requires the 'authtoken'
964     authtoken : Authentication token string;  When actions are performed that require a user login
965         (e.g. tagging circulated records), the authentication token is required
966
967 The searches element is required, must have a hashref value, and the hashref must contain at least one 
968 of the following classes as a key:
969
970     title
971     author
972     subject
973     series
974     keyword
975
976 The value paired with a key is the associated search string.
977
978 The docache argument enables/disables searching and saving results in cache (default OFF).
979
980 The return object, if successful, will look like:
981
982     { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
983
984 =cut
985
986 __PACKAGE__->register_method(
987     method    => 'the_quest_for_knowledge',
988     api_name  => 'open-ils.search.biblio.multiclass.staff',
989     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
990 );
991 __PACKAGE__->register_method(
992     method    => 'the_quest_for_knowledge',
993     api_name  => 'open-ils.search.metabib.multiclass',
994     signature => q/@see open-ils.search.biblio.multiclass/
995 );
996 __PACKAGE__->register_method(
997     method    => 'the_quest_for_knowledge',
998     api_name  => 'open-ils.search.metabib.multiclass.staff',
999     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
1000 );
1001
1002 sub the_quest_for_knowledge {
1003     my( $self, $conn, $searchhash, $docache ) = @_;
1004
1005     return { count => 0 } unless $searchhash and
1006         ref $searchhash->{searches} eq 'HASH';
1007
1008     my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1009     my $ismeta = 0;
1010     my @recs;
1011
1012     if($self->api_name =~ /metabib/) {
1013         $ismeta = 1;
1014         $method =~ s/biblio/metabib/o;
1015     }
1016
1017     # do some simple sanity checking
1018     if(!$searchhash->{searches} or
1019         ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1020         return { count => 0 };
1021     }
1022
1023     my $offset = $searchhash->{offset} ||  0;   # user value or default in local var now
1024     my $limit  = $searchhash->{limit}  || 10;   # user value or default in local var now
1025     my $end    = $offset + $limit - 1;
1026
1027     my $maxlimit = 5000;
1028     $searchhash->{offset} = 0;                  # possible user value overwritten in hash
1029     $searchhash->{limit}  = $maxlimit;          # possible user value overwritten in hash
1030
1031     return { count => 0 } if $offset > $maxlimit;
1032
1033     my @search;
1034     push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1035     my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1036     my $ckey = $pfx . md5_hex($method . $s);
1037
1038     $logger->info("bib search for: $s");
1039
1040     $searchhash->{limit} -= $offset;
1041
1042
1043     my $trim = 0;
1044     my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1045
1046     if(!$result) {
1047
1048         $method .= ".staff" if($self->api_name =~ /staff/);
1049         $method .= ".atomic";
1050     
1051         for (keys %$searchhash) { 
1052             delete $$searchhash{$_} 
1053                 unless defined $$searchhash{$_}; 
1054         }
1055     
1056         $result = $U->storagereq( $method, %$searchhash );
1057         $trim = 1;
1058
1059     } else { 
1060         $docache = 0;   # results came FROM cache, so we don't write back
1061     }
1062
1063     return {count => 0} unless ($result && $$result[0]);
1064
1065     @recs = @$result;
1066
1067     my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1068
1069     if($docache) {
1070         # If we didn't get this data from the cache, put it into the cache
1071         # then return the correct offset of records
1072         $logger->debug("putting search cache $ckey\n");
1073         put_cache($ckey, $count, \@recs);
1074     }
1075
1076     if($trim) {
1077         # if we have the full set of data, trim out 
1078         # the requested chunk based on limit and offset
1079         my @t;
1080         for ($offset..$end) {
1081             last unless $recs[$_];
1082             push(@t, $recs[$_]);
1083         }
1084         @recs = @t;
1085     }
1086
1087     return { ids => \@recs, count => $count };
1088 }
1089
1090
1091 __PACKAGE__->register_method(
1092     method    => 'staged_search',
1093     api_name  => 'open-ils.search.biblio.multiclass.staged',
1094     signature => {
1095         desc   => 'Staged search filters out unavailable items.  This means that it relies on an estimation strategy for determining ' .
1096                   'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering.  See "estimation_strategy" in your SRF config.',
1097         params => [
1098             {
1099                 desc => "A search hash with keys: "
1100                       . "searches, limit, offset.  The others are optional, but the 'searches' key/value pair is required, with the value being a hashref.  "
1101                       . "See perldoc " . __PACKAGE__ . " for more detail",
1102                 type => 'object',
1103             },
1104             {
1105                 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1106                 type => 'string',
1107             }
1108         ],
1109         return => {
1110             desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids.  '
1111                   . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1112             type => 'object',
1113         }
1114     }
1115 );
1116 __PACKAGE__->register_method(
1117     method    => 'staged_search',
1118     api_name  => 'open-ils.search.biblio.multiclass.staged.staff',
1119     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1120 );
1121 __PACKAGE__->register_method(
1122     method    => 'staged_search',
1123     api_name  => 'open-ils.search.metabib.multiclass.staged',
1124     signature => q/@see open-ils.search.biblio.multiclass.staged/
1125 );
1126 __PACKAGE__->register_method(
1127     method    => 'staged_search',
1128     api_name  => 'open-ils.search.metabib.multiclass.staged.staff',
1129     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1130 );
1131
1132 my $estimation_strategy;
1133 sub staged_search {
1134     my($self, $conn, $search_hash, $docache) = @_;
1135
1136     my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1137
1138     my $method = $IAmMetabib?
1139         'open-ils.storage.metabib.multiclass.staged.search_fts':
1140         'open-ils.storage.biblio.multiclass.staged.search_fts';
1141
1142     $method .= '.staff' if $self->api_name =~ /staff$/;
1143     $method .= '.atomic';
1144                 
1145     if (!$search_hash->{query}) {
1146         return {count => 0} unless (
1147             $search_hash and 
1148             $search_hash->{searches} and 
1149             scalar( keys %{$search_hash->{searches}} ));
1150     }
1151
1152     my $search_duration;
1153     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
1154     my $user_limit  = $search_hash->{limit}  || 10;
1155     my $ignore_facet_classes  = $search_hash->{ignore_facet_classes};
1156     $user_offset = ($user_offset >= 0) ? $user_offset :  0;
1157     $user_limit  = ($user_limit  >= 0) ? $user_limit  : 10;
1158
1159
1160     # we're grabbing results on a per-superpage basis, which means the 
1161     # limit and offset should coincide with superpage boundaries
1162     $search_hash->{offset} = 0;
1163     $search_hash->{limit} = $superpage_size;
1164
1165     # force a well-known check_limit
1166     $search_hash->{check_limit} = $superpage_size; 
1167     # restrict total tested to superpage size * number of superpages
1168     $search_hash->{core_limit}  = $superpage_size * $max_superpages;
1169
1170     # Set the configured estimation strategy, defaults to 'inclusion'.
1171     unless ($estimation_strategy) {
1172         $estimation_strategy = OpenSRF::Utils::SettingsClient
1173             ->new
1174             ->config_value(
1175                 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1176             ) || 'inclusion';
1177     }
1178     $search_hash->{estimation_strategy} = $estimation_strategy;
1179
1180     # pull any existing results from the cache
1181     my $key = search_cache_key($method, $search_hash);
1182     my $facet_key = $key.'_facets';
1183     my $cache_data = $cache->get_cache($key) || {};
1184
1185     # First, we want to make sure that someone else isn't currently trying to perform exactly
1186     # this same search.  The point is to allow just one instance of a search to fill the needs
1187     # of all concurrent, identical searches.  This will avoid spammy searches killing the
1188     # database without requiring admins to start locking some IP addresses out entirely.
1189     #
1190     # There's still a tiny race condition where 2 might run, but without sigificantly more code
1191     # and complexity, this is close to the best we can do.
1192
1193     if ($cache_data->{running}) { # someone is already doing the search...
1194         my $stop_looping = time() + $cache_timeout;
1195         while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1196             $cache_data = $cache->get_cache($key) || {};
1197             last if (!$cache_data->{running});
1198         }
1199     } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1200         $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1201     }
1202
1203     # keep retrieving results until we find enough to 
1204     # fulfill the user-specified limit and offset
1205     my $all_results = [];
1206     my $page; # current superpage
1207     my $current_page_summary = {};
1208     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1209     my $new_ids = [];
1210
1211     for($page = 0; $page < $max_superpages; $page++) {
1212
1213         my $data = $cache_data->{$page};
1214         my $results;
1215         my $summary;
1216
1217         $logger->debug("staged search: analyzing superpage $page");
1218
1219         if($data) {
1220             # this window of results is already cached
1221             $logger->debug("staged search: found cached results");
1222             $summary = $data->{summary};
1223             $results = $data->{results};
1224
1225         } else {
1226             # retrieve the window of results from the database
1227             $logger->debug("staged search: fetching results from the database");
1228             $search_hash->{skip_check} = $page * $superpage_size;
1229             $search_hash->{return_query} = $page == 0 ? 1 : 0;
1230
1231             my $start = time;
1232             $results = $U->storagereq($method, %$search_hash);
1233             $search_duration = time - $start;
1234             $summary = shift(@$results) if $results;
1235
1236             unless($summary) {
1237                 $logger->info("search timed out: duration=$search_duration: params=".
1238                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1239                 return {count => 0};
1240             }
1241
1242             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1243
1244             # Create backwards-compatible result structures
1245             if($IAmMetabib) {
1246                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1247             } else {
1248                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1249             }
1250
1251             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1252             $results = [grep {defined $_->[0]} @$results];
1253             cache_staged_search_page($key, $page, $summary, $results) if $docache;
1254         }
1255
1256         tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib) 
1257             if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1258
1259         $current_page_summary = $summary;
1260
1261         # add the new set of results to the set under construction
1262         push(@$all_results, @$results);
1263
1264         my $current_count = scalar(@$all_results);
1265
1266         if ($page == 0) { # all summaries are the same, just get the first
1267             for (keys %$summary) {
1268                 $global_summary->{$_} = $summary->{$_};
1269             }
1270         }
1271
1272         # we've found all the possible hits
1273         last if $current_count == $summary->{visible};
1274
1275         # we've found enough results to satisfy the requested limit/offset
1276         last if $current_count >= ($user_limit + $user_offset);
1277
1278         # we've scanned all possible hits
1279         last if($summary->{checked} < $superpage_size);
1280     }
1281
1282     # Let other backends grab our data now that we're done.
1283     $cache_data = $cache->get_cache($key);
1284     if ($$cache_data{running} and $$cache_data{running} == $$) {
1285         delete $$cache_data{running};
1286         $cache->put_cache($key, $cache_data, $cache_timeout);
1287     }
1288
1289     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1290
1291     $conn->respond_complete(
1292         {
1293             global_summary    => $global_summary,
1294             count             => $global_summary->{visible},
1295             core_limit        => $search_hash->{core_limit},
1296             superpage         => $page,
1297             superpage_size    => $search_hash->{check_limit},
1298             superpage_summary => $current_page_summary,
1299             facet_key         => $facet_key,
1300             ids               => \@results
1301         }
1302     );
1303
1304     $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1305
1306     return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1307 }
1308
1309 sub passthrough_fetch_display_fields {
1310     my $self = shift;
1311     my $conn = shift;
1312     my $highlight_map = shift;
1313     my @records = @_;
1314
1315     return $U->storagereq(
1316         'open-ils.storage.fetch.metabib.display_field.highlight',
1317         $highlight_map,
1318         @records
1319     ) if (@records == 1);
1320
1321     return $U->storagereq(
1322         'open-ils.storage.fetch.metabib.display_field.highlight.atomic',
1323         $highlight_map,
1324         \@records
1325     );
1326 }
1327 __PACKAGE__->register_method(
1328     method    => 'passthrough_fetch_display_fields',
1329     api_name  => 'open-ils.search.fetch.metabib.display_field.highlight'
1330 );
1331
1332
1333 sub tag_circulated_records {
1334     my ($auth, $results, $metabib) = @_;
1335     my $e = new_editor(authtoken => $auth);
1336     return $results unless $e->checkauth;
1337
1338     my $query = {
1339         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1340         from     => { auch => { acp => { join => 'acn' }} }, 
1341         where    => { usr => $e->requestor->id },
1342         distinct => 1
1343     };
1344
1345     if ($metabib) {
1346         $query = {
1347             select   => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1348             from     => 'mmrsm',
1349             where    => { source => { in => $query } },
1350             distinct => 1
1351         };
1352     }
1353
1354     # Give me the distinct set of bib records that exist in the user's visible circulation history
1355     my $circ_recs = $e->json_query( $query );
1356
1357     # if the record appears in the circ history, push a 1 onto 
1358     # the rec array structure to indicate truthiness
1359     for my $rec (@$results) {
1360         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1361     }
1362
1363     $results
1364 }
1365
1366 # creates a unique token to represent the query in the cache
1367 sub search_cache_key {
1368     my $method = shift;
1369     my $search_hash = shift;
1370     my @sorted;
1371     for my $key (sort keys %$search_hash) {
1372         push(@sorted, ($key => $$search_hash{$key})) 
1373             unless $key eq 'limit'  or 
1374                    $key eq 'offset' or 
1375                    $key eq 'skip_check';
1376     }
1377     my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1378     return $pfx . md5_hex($method . $s);
1379 }
1380
1381 sub retrieve_cached_facets {
1382     my $self   = shift;
1383     my $client = shift;
1384     my $key    = shift;
1385     my $limit    = shift;
1386
1387     return undef unless ($key and $key =~ /_facets$/);
1388
1389     eval {
1390         local $SIG{ALRM} = sub {die};
1391         alarm(10); # we'll sleep for as much as 10s
1392         do {
1393             die if $cache->get_cache($key . '_COMPLETE');
1394         } while (sleep(0.05));
1395         alarm(0);
1396     };
1397     alarm(0);
1398
1399     my $blob = $cache->get_cache($key) || {};
1400
1401     my $facets = {};
1402     if ($limit) {
1403        for my $f ( keys %$blob ) {
1404             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1405             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1406             for my $s ( @sorted ) {
1407                 my ($k) = keys(%$s);
1408                 my ($v) = values(%$s);
1409                 $$facets{$f}{$k} = $v;
1410             }
1411         }
1412     } else {
1413         $facets = $blob;
1414     }
1415
1416     return $facets;
1417 }
1418
1419 __PACKAGE__->register_method(
1420     method   => "retrieve_cached_facets",
1421     api_name => "open-ils.search.facet_cache.retrieve",
1422     signature => {
1423         desc   => 'Returns facet data derived from a specific search based on a key '.
1424                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1425         params => [
1426             {
1427                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1428                 type => 'string',
1429             }
1430         ],
1431         return => {
1432             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1433                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1434                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1435                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1436             type => 'object',
1437         }
1438     }
1439 );
1440
1441
1442 sub cache_facets {
1443     # add facets for this search to the facet cache
1444     my($key, $results, $metabib, $ignore) = @_;
1445     my $data = $cache->get_cache($key);
1446     $data ||= {};
1447
1448     return undef unless (@$results);
1449
1450     my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1451                                    : 'search.facets_for_record_set';
1452     my $results_str = '{' . join(',', @$results) . '}';
1453     my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1454                                   : '{}';
1455     my $query = {   
1456         from => [ $facets_function, $ignore_str, $results_str ]
1457     };
1458
1459     my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1460
1461     for my $facet (@$facets) {
1462         next unless ($facet->{value});
1463         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1464     }
1465
1466     $logger->info("facet compilation: cached with key=$key");
1467
1468     $cache->put_cache($key, $data, $cache_timeout);
1469     $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1470 }
1471
1472 sub cache_staged_search_page {
1473     # puts this set of results into the cache
1474     my($key, $page, $summary, $results) = @_;
1475     my $data = $cache->get_cache($key);
1476     $data ||= {};
1477     $data->{$page} = {
1478         summary => $summary,
1479         results => $results
1480     };
1481
1482     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1483         ($summary->{estimated_hit_count} || "none") .
1484         ", visible=" . ($summary->{visible} || "none")
1485     );
1486
1487     $cache->put_cache($key, $data, $cache_timeout);
1488 }
1489
1490 sub search_cache {
1491
1492     my $key     = shift;
1493     my $offset  = shift;
1494     my $limit   = shift;
1495     my $start   = $offset;
1496     my $end     = $offset + $limit - 1;
1497
1498     $logger->debug("searching cache for $key : $start..$end\n");
1499
1500     return undef unless $cache;
1501     my $data = $cache->get_cache($key);
1502
1503     return undef unless $data;
1504
1505     my $count = $data->[0];
1506     $data = $data->[1];
1507
1508     return undef unless $offset < $count;
1509
1510     my @result;
1511     for( my $i = $offset; $i <= $end; $i++ ) {
1512         last unless my $d = $$data[$i];
1513         push( @result, $d );
1514     }
1515
1516     $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1517
1518     return \@result;
1519 }
1520
1521
1522 sub put_cache {
1523     my( $key, $count, $data ) = @_;
1524     return undef unless $cache;
1525     $logger->debug("search_cache putting ".
1526         scalar(@$data)." items at key $key with timeout $cache_timeout");
1527     $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1528 }
1529
1530
1531 __PACKAGE__->register_method(
1532     method   => "biblio_mrid_to_modsbatch_batch",
1533     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1534 );
1535
1536 sub biblio_mrid_to_modsbatch_batch {
1537     my( $self, $client, $mrids) = @_;
1538     # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1539     my @mods;
1540     my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1541     for my $id (@$mrids) {
1542         next unless defined $id;
1543         my ($m) = $method->run($id);
1544         push @mods, $m;
1545     }
1546     return \@mods;
1547 }
1548
1549
1550 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1551              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1552     {
1553     __PACKAGE__->register_method(
1554         method    => "biblio_mrid_to_modsbatch",
1555         api_name  => $_,
1556         signature => {
1557             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1558                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1559             params => [
1560                 { desc => 'Metarecord ID', type => 'number' },
1561                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1562             ],
1563             return => {
1564                 desc => 'MVR Object, event on error',
1565             }
1566         }
1567     );
1568 }
1569
1570 sub biblio_mrid_to_modsbatch {
1571     my( $self, $client, $mrid, $args) = @_;
1572
1573     # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1574
1575     my ($mr, $evt) = _grab_metarecord($mrid);
1576     return $evt unless $mr;
1577
1578     my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1579               biblio_mrid_make_modsbatch($self, $client, $mr);
1580
1581     return $mvr unless ref($args);  
1582
1583     # Here we find the lead record appropriate for the given filters 
1584     # and use that for the title and author of the metarecord
1585     my $format = $$args{format};
1586     my $org    = $$args{org};
1587     my $depth  = $$args{depth};
1588
1589     return $mvr unless $format or $org or $depth;
1590
1591     my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1592     $method = "$method.staff" if $self->api_name =~ /staff/o; 
1593
1594     my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1595
1596     if( my $mods = $U->record_to_mvr($rec) ) {
1597
1598         $mvr->title( $mods->title );
1599         $mvr->author($mods->author);
1600         $logger->debug("mods_slim updating title and ".
1601             "author in mvr with ".$mods->title." : ".$mods->author);
1602     }
1603
1604     return $mvr;
1605 }
1606
1607 # converts a metarecord to an mvr
1608 sub _mr_to_mvr {
1609     my $mr = shift;
1610     my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1611     return Fieldmapper::metabib::virtual_record->new($perl);
1612 }
1613
1614 # checks to see if a metarecord has mods, if so returns true;
1615
1616 __PACKAGE__->register_method(
1617     method   => "biblio_mrid_check_mvr",
1618     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1619     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1620               . "if the metarecord already has an mvr associated with it."
1621 );
1622
1623 sub biblio_mrid_check_mvr {
1624     my( $self, $client, $mrid ) = @_;
1625     my $mr; 
1626
1627     my $evt;
1628     if(ref($mrid)) { $mr = $mrid; } 
1629     else { ($mr, $evt) = _grab_metarecord($mrid); }
1630     return $evt if $evt;
1631
1632     # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1633
1634     return _mr_to_mvr($mr) if $mr->mods();
1635     return undef;
1636 }
1637
1638 sub _grab_metarecord {
1639     my $mrid = shift;
1640     my $e = new_editor();
1641     my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1642     return ($mr);
1643 }
1644
1645
1646 __PACKAGE__->register_method(
1647     method   => "biblio_mrid_make_modsbatch",
1648     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1649     notes    => "Takes either a metarecord ID or a metarecord object. "
1650               . "Forces the creations of an mvr for the given metarecord. "
1651               . "The created mvr is returned."
1652 );
1653
1654 sub biblio_mrid_make_modsbatch {
1655     my( $self, $client, $mrid ) = @_;
1656
1657     my $e = new_editor();
1658
1659     my $mr;
1660     if( ref($mrid) ) {
1661         $mr = $mrid;
1662         $mrid = $mr->id;
1663     } else {
1664         $mr = $e->retrieve_metabib_metarecord($mrid) 
1665             or return $e->event;
1666     }
1667
1668     my $masterid = $mr->master_record;
1669     $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1670
1671     my $ids = $U->storagereq(
1672         'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1673     return undef unless @$ids;
1674
1675     my $master = $e->retrieve_biblio_record_entry($masterid)
1676         or return $e->event;
1677
1678     # start the mods batch
1679     my $u = OpenILS::Utils::ModsParser->new();
1680     $u->start_mods_batch( $master->marc );
1681
1682     # grab all of the sub-records and shove them into the batch
1683     my @ids = grep { $_ ne $masterid } @$ids;
1684     #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1685
1686     my $subrecs = [];
1687     if(@$ids) {
1688         for my $i (@$ids) {
1689             my $r = $e->retrieve_biblio_record_entry($i);
1690             push( @$subrecs, $r ) if $r;
1691         }
1692     }
1693
1694     for(@$subrecs) {
1695         $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1696         $u->push_mods_batch( $_->marc ) if $_->marc;
1697     }
1698
1699
1700     # finish up and send to the client
1701     my $mods = $u->finish_mods_batch();
1702     $mods->doc_id($mrid);
1703     $client->respond_complete($mods);
1704
1705
1706     # now update the mods string in the db
1707     my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1708     $mr->mods($string);
1709
1710     $e = new_editor(xact => 1);
1711     $e->update_metabib_metarecord($mr) 
1712         or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1713     $e->finish;
1714
1715     return undef;
1716 }
1717
1718
1719 # converts a mr id into a list of record ids
1720
1721 foreach (qw/open-ils.search.biblio.metarecord_to_records
1722             open-ils.search.biblio.metarecord_to_records.staff/)
1723 {
1724     __PACKAGE__->register_method(
1725         method    => "biblio_mrid_to_record_ids",
1726         api_name  => $_,
1727         signature => {
1728             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1729                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1730             params => [
1731                 { desc => 'Metarecord ID', type => 'number' },
1732                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1733             ],
1734             return => {
1735                 desc => 'Results object like {count => $i, ids =>[...]}',
1736                 type => 'object'
1737             }
1738             
1739         }
1740     );
1741 }
1742
1743 sub biblio_mrid_to_record_ids {
1744     my( $self, $client, $mrid, $args ) = @_;
1745
1746     my $format = $$args{format};
1747     my $org    = $$args{org};
1748     my $depth  = $$args{depth};
1749
1750     my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1751     $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1752     my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1753
1754     return { count => scalar(@$recs), ids => $recs };
1755 }
1756
1757
1758 __PACKAGE__->register_method(
1759     method   => "biblio_record_to_marc_html",
1760     api_name => "open-ils.search.biblio.record.html"
1761 );
1762
1763 __PACKAGE__->register_method(
1764     method   => "biblio_record_to_marc_html",
1765     api_name => "open-ils.search.authority.to_html"
1766 );
1767
1768 # Persistent parsers and setting objects
1769 my $parser = XML::LibXML->new();
1770 my $xslt   = XML::LibXSLT->new();
1771 my $marc_sheet;
1772 my $slim_marc_sheet;
1773 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1774
1775 sub biblio_record_to_marc_html {
1776     my($self, $client, $recordid, $slim, $marcxml) = @_;
1777
1778     my $sheet;
1779     my $dir = $settings_client->config_value("dirs", "xsl");
1780
1781     if($slim) {
1782         unless($slim_marc_sheet) {
1783             my $xsl = $settings_client->config_value(
1784                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1785             if($xsl) {
1786                 $xsl = $parser->parse_file("$dir/$xsl");
1787                 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1788             }
1789         }
1790         $sheet = $slim_marc_sheet;
1791     }
1792
1793     unless($sheet) {
1794         unless($marc_sheet) {
1795             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1796             my $xsl = $settings_client->config_value(
1797                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1798             $xsl = $parser->parse_file("$dir/$xsl");
1799             $marc_sheet = $xslt->parse_stylesheet($xsl);
1800         }
1801         $sheet = $marc_sheet;
1802     }
1803
1804     my $record;
1805     unless($marcxml) {
1806         my $e = new_editor();
1807         if($self->api_name =~ /authority/) {
1808             $record = $e->retrieve_authority_record_entry($recordid)
1809                 or return $e->event;
1810         } else {
1811             $record = $e->retrieve_biblio_record_entry($recordid)
1812                 or return $e->event;
1813         }
1814         $marcxml = $record->marc;
1815     }
1816
1817     my $xmldoc = $parser->parse_string($marcxml);
1818     my $html = $sheet->transform($xmldoc);
1819     return $html->documentElement->toString();
1820 }
1821
1822 __PACKAGE__->register_method(
1823     method    => "format_biblio_record_entry",
1824     api_name  => "open-ils.search.biblio.record.print",
1825     signature => {
1826         desc   => 'Returns a printable version of the specified bib record',
1827         params => [
1828             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1829         ],
1830         return => {
1831             desc => q/An action_trigger.event object or error event./,
1832             type => 'object',
1833         }
1834     }
1835 );
1836 __PACKAGE__->register_method(
1837     method    => "format_biblio_record_entry",
1838     api_name  => "open-ils.search.biblio.record.email",
1839     signature => {
1840         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1841         params => [
1842             { desc => 'Authentication token',  type => 'string'},
1843             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1844         ],
1845         return => {
1846             desc => q/Undefined on success, otherwise an error event./,
1847             type => 'object',
1848         }
1849     }
1850 );
1851
1852 sub format_biblio_record_entry {
1853     my($self, $conn, $arg1, $arg2) = @_;
1854
1855     my $for_print = ($self->api_name =~ /print/);
1856     my $for_email = ($self->api_name =~ /email/);
1857
1858     my $e; my $auth; my $bib_id; my $context_org;
1859
1860     if ($for_print) {
1861         $bib_id = $arg1;
1862         $context_org = $arg2 || $U->get_org_tree->id;
1863         $e = new_editor(xact => 1);
1864     } elsif ($for_email) {
1865         $auth = $arg1;
1866         $bib_id = $arg2;
1867         $e = new_editor(authtoken => $auth, xact => 1);
1868         return $e->die_event unless $e->checkauth;
1869         $context_org = $e->requestor->home_ou;
1870     }
1871
1872     my $bib_ids;
1873     if (ref $bib_id ne 'ARRAY') {
1874         $bib_ids = [ $bib_id ];
1875     } else {
1876         $bib_ids = $bib_id;
1877     }
1878
1879     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1880     $bucket->btype('temp');
1881     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1882     if ($for_email) {
1883         $bucket->owner($e->requestor) 
1884     } else {
1885         $bucket->owner(1);
1886     }
1887     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1888
1889     for my $id (@$bib_ids) {
1890
1891         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1892
1893         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1894         $bucket_entry->target_biblio_record_entry($bib);
1895         $bucket_entry->bucket($bucket_obj->id);
1896         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1897     }
1898
1899     $e->commit;
1900
1901     if ($for_print) {
1902
1903         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1904
1905     } elsif ($for_email) {
1906
1907         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1908     }
1909
1910     return undef;
1911 }
1912
1913
1914 __PACKAGE__->register_method(
1915     method   => "retrieve_all_copy_statuses",
1916     api_name => "open-ils.search.config.copy_status.retrieve.all"
1917 );
1918
1919 sub retrieve_all_copy_statuses {
1920     my( $self, $client ) = @_;
1921     return new_editor()->retrieve_all_config_copy_status();
1922 }
1923
1924
1925 __PACKAGE__->register_method(
1926     method   => "copy_counts_per_org",
1927     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1928 );
1929
1930 __PACKAGE__->register_method(
1931     method   => "copy_counts_per_org",
1932     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1933 );
1934
1935 sub copy_counts_per_org {
1936     my( $self, $client, $record_id ) = @_;
1937
1938     warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1939
1940     my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1941     if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1942
1943     my $counts = $apputils->simple_scalar_request(
1944         "open-ils.storage", $method, $record_id );
1945
1946     $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1947     return $counts;
1948 }
1949
1950
1951 __PACKAGE__->register_method(
1952     method   => "copy_count_summary",
1953     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1954     notes    => "returns an array of these: "
1955               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1956               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1957 );
1958         
1959
1960 sub copy_count_summary {
1961     my( $self, $client, $rid, $org, $depth ) = @_;
1962     $org   ||= 1;
1963     $depth ||= 0;
1964     my $data = $U->storagereq(
1965         'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1966
1967     return [ sort {
1968         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
1969         cmp
1970         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
1971     } @$data ];
1972 }
1973
1974 __PACKAGE__->register_method(
1975     method   => "copy_location_count_summary",
1976     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1977     notes    => "returns an array of these: "
1978               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
1979               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1980 );
1981
1982 sub copy_location_count_summary {
1983     my( $self, $client, $rid, $org, $depth ) = @_;
1984     $org   ||= 1;
1985     $depth ||= 0;
1986     my $data = $U->storagereq(
1987         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1988
1989     return [ sort {
1990         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
1991         cmp
1992         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
1993
1994         || $a->[4] cmp $b->[4]
1995     } @$data ];
1996 }
1997
1998 __PACKAGE__->register_method(
1999     method   => "copy_count_location_summary",
2000     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2001     notes    => "returns an array of these: "
2002               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2003               . "where statusx is a copy status name.  The statuses are sorted by ID."
2004 );
2005
2006 sub copy_count_location_summary {
2007     my( $self, $client, $rid, $org, $depth ) = @_;
2008     $org   ||= 1;
2009     $depth ||= 0;
2010     my $data = $U->storagereq(
2011         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2012     return [ sort {
2013         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2014         cmp
2015         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2016     } @$data ];
2017 }
2018
2019
2020 foreach (qw/open-ils.search.biblio.marc
2021             open-ils.search.biblio.marc.staff/)
2022 {
2023 __PACKAGE__->register_method(
2024     method    => "marc_search",
2025     api_name  => $_,
2026     signature => {
2027         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2028                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2029         params => [
2030             {
2031                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2032                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2033                 type => 'object'
2034             },
2035             {desc => 'timeout (optional)',  type => 'number'}
2036         ],
2037         return => {
2038             desc => 'Results object like: { "count": $i, "ids": [...] }',
2039             type => 'object'
2040         }
2041     }
2042 );
2043 }
2044
2045 =head3 open-ils.search.biblio.marc (arghash, timeout)
2046
2047 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2048
2049     searches: complex query object  (required)
2050     org_unit: The org ID to focus the search at
2051     depth   : The org depth     
2052     limit   : integer search limit      default: 10
2053     offset  : integer search offset     default:  0
2054     sort    : What field to sort the results on? [ author | title | pubdate ]
2055     sort_dir: In what direction do we sort? [ asc | desc ]
2056
2057 Additional keys to refine search criteria:
2058
2059     audience : Audience
2060     language : Language (code)
2061     lit_form : Literary form
2062     item_form: Item form
2063     item_type: Item type
2064     format   : The MARC format
2065
2066 Please note that the specific strings to be used in the "addtional keys" will be entirely
2067 dependent on your loaded data.  
2068
2069 All keys except "searches" are optional.
2070 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2071
2072 For example, an arg hash might look like:
2073
2074     $arghash = {
2075         searches => [
2076             {
2077                 term     => "harry",
2078                 restrict => [
2079                     {
2080                         tag => 245,
2081                         subfield => "a"
2082                     }
2083                     # ...
2084                 ]
2085             }
2086             # ...
2087         ],
2088         org_unit  => 1,
2089         limit     => 5,
2090         sort      => "author",
2091         item_type => "g"
2092     }
2093
2094 The arghash is eventually passed to the SRF call:
2095 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2096
2097 Presently, search uses the cache unconditionally.
2098
2099 =cut
2100
2101 # FIXME: that example above isn't actually tested.
2102 # FIXME: sort and limit added.  item_type not tested yet.
2103 # TODO: docache option?
2104 sub marc_search {
2105     my( $self, $conn, $args, $timeout ) = @_;
2106
2107     my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2108     $method .= ".staff" if $self->api_name =~ /staff/;
2109     $method .= ".atomic";
2110
2111     my $limit = $args->{limit} || 10;
2112     my $offset = $args->{offset} || 0;
2113
2114     # allow caller to pass in a call timeout since MARC searches
2115     # can take longer than the default 60-second timeout.  
2116     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2117     $timeout = 120 if !$timeout or $timeout > 300;
2118
2119     my @search;
2120     push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2121     my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2122
2123     my $recs = search_cache($ckey, $offset, $limit);
2124
2125     if(!$recs) {
2126
2127         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2128         my $req = $ses->request($method, %$args);
2129         my $resp = $req->recv($timeout);
2130
2131         if($resp and $recs = $resp->content) {
2132             put_cache($ckey, scalar(@$recs), $recs);
2133         } else {
2134             $recs = [];
2135         }
2136
2137         $ses->kill_me;
2138     }
2139
2140     my $count = 0;
2141     $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2142     my @recs = map { $_->[0] } @$recs;
2143
2144     return { ids => \@recs, count => $count };
2145 }
2146
2147
2148 foreach my $isbn_method (qw/
2149     open-ils.search.biblio.isbn
2150     open-ils.search.biblio.isbn.staff
2151 /) {
2152 __PACKAGE__->register_method(
2153     method    => "biblio_search_isbn",
2154     api_name  => $isbn_method,
2155     signature => {
2156         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2157         params => [
2158             {desc => 'ISBN', type => 'string'}
2159         ],
2160         return => {
2161             desc => 'Results object like: { "count": $i, "ids": [...] }',
2162             type => 'object'
2163         }
2164     }
2165 );
2166 }
2167
2168 sub biblio_search_isbn { 
2169     my( $self, $client, $isbn ) = @_;
2170     $logger->debug("Searching ISBN $isbn");
2171     # the previous implementation of this method was essentially unlimited,
2172     # so we will set our limit very high and let multiclass.query provide any
2173     # actual limit
2174     # XXX: if making this unlimited is deemed important, we might consider
2175     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2176     # which is functionally deprecated at this point, or a custom call to
2177     # 'open-ils.storage.biblio.multiclass.search_fts'
2178
2179     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2180     if ($self->api_name =~ m/.staff$/) {
2181         $isbn_method .= '.staff';
2182     }
2183
2184     my $method = $self->method_lookup($isbn_method);
2185     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2186     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2187     return { ids => \@recs, count => $search_result->{'count'} };
2188 }
2189
2190 __PACKAGE__->register_method(
2191     method   => "biblio_search_isbn_batch",
2192     api_name => "open-ils.search.biblio.isbn_list",
2193 );
2194
2195 # XXX: see biblio_search_isbn() for note concerning 'limit'
2196 sub biblio_search_isbn_batch { 
2197     my( $self, $client, $isbn_list ) = @_;
2198     $logger->debug("Searching ISBNs @$isbn_list");
2199     my @recs = (); my %rec_set = ();
2200     my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2201     foreach my $isbn ( @$isbn_list ) {
2202         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2203         my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2204         foreach my $rec (@recs_subset) {
2205             if (! $rec_set{ $rec }) {
2206                 $rec_set{ $rec } = 1;
2207                 push @recs, $rec;
2208             }
2209         }
2210     }
2211     return { ids => \@recs, count => scalar(@recs) };
2212 }
2213
2214 foreach my $issn_method (qw/
2215     open-ils.search.biblio.issn
2216     open-ils.search.biblio.issn.staff
2217 /) {
2218 __PACKAGE__->register_method(
2219     method   => "biblio_search_issn",
2220     api_name => $issn_method,
2221     signature => {
2222         desc   => 'Retrieve biblio IDs for a given ISSN',
2223         params => [
2224             {desc => 'ISBN', type => 'string'}
2225         ],
2226         return => {
2227             desc => 'Results object like: { "count": $i, "ids": [...] }',
2228             type => 'object'
2229         }
2230     }
2231 );
2232 }
2233
2234 sub biblio_search_issn { 
2235     my( $self, $client, $issn ) = @_;
2236     $logger->debug("Searching ISSN $issn");
2237     # the previous implementation of this method was essentially unlimited,
2238     # so we will set our limit very high and let multiclass.query provide any
2239     # actual limit
2240     # XXX: if making this unlimited is deemed important, we might consider
2241     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2242     # which is functionally deprecated at this point, or a custom call to
2243     # 'open-ils.storage.biblio.multiclass.search_fts'
2244
2245     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2246     if ($self->api_name =~ m/.staff$/) {
2247         $issn_method .= '.staff';
2248     }
2249
2250     my $method = $self->method_lookup($issn_method);
2251     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2252     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2253     return { ids => \@recs, count => $search_result->{'count'} };
2254 }
2255
2256
2257 __PACKAGE__->register_method(
2258     method    => "fetch_mods_by_copy",
2259     api_name  => "open-ils.search.biblio.mods_from_copy",
2260     argc      => 1,
2261     signature => {
2262         desc    => 'Retrieve MODS record given an attached copy ID',
2263         params  => [
2264             { desc => 'Copy ID', type => 'number' }
2265         ],
2266         returns => {
2267             desc => 'MODS record, event on error or uncataloged item'
2268         }
2269     }
2270 );
2271
2272 sub fetch_mods_by_copy {
2273     my( $self, $client, $copyid ) = @_;
2274     my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2275     return $evt if $evt;
2276     return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2277     return $apputils->record_to_mvr($record);
2278 }
2279
2280
2281 # -------------------------------------------------------------------------------------
2282
2283 __PACKAGE__->register_method(
2284     method   => "cn_browse",
2285     api_name => "open-ils.search.callnumber.browse.target",
2286     notes    => "Starts a callnumber browse"
2287 );
2288
2289 __PACKAGE__->register_method(
2290     method   => "cn_browse",
2291     api_name => "open-ils.search.callnumber.browse.page_up",
2292     notes    => "Returns the previous page of callnumbers",
2293 );
2294
2295 __PACKAGE__->register_method(
2296     method   => "cn_browse",
2297     api_name => "open-ils.search.callnumber.browse.page_down",
2298     notes    => "Returns the next page of callnumbers",
2299 );
2300
2301
2302 # RETURNS array of arrays like so: label, owning_lib, record, id
2303 sub cn_browse {
2304     my( $self, $client, @params ) = @_;
2305     my $method;
2306
2307     $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2308         if( $self->api_name =~ /target/ );
2309     $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2310         if( $self->api_name =~ /page_up/ );
2311     $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2312         if( $self->api_name =~ /page_down/ );
2313
2314     return $apputils->simplereq( 'open-ils.storage', $method, @params );
2315 }
2316 # -------------------------------------------------------------------------------------
2317
2318 __PACKAGE__->register_method(
2319     method        => "fetch_cn",
2320     api_name      => "open-ils.search.callnumber.retrieve",
2321     authoritative => 1,
2322     notes         => "retrieves a callnumber based on ID",
2323 );
2324
2325 sub fetch_cn {
2326     my( $self, $client, $id ) = @_;
2327
2328     my $e = new_editor();
2329     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2330     return $evt if $evt;
2331     return $cn;
2332 }
2333
2334 __PACKAGE__->register_method(
2335     method        => "fetch_fleshed_cn",
2336     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2337     authoritative => 1,
2338     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2339 );
2340
2341 sub fetch_fleshed_cn {
2342     my( $self, $client, $id ) = @_;
2343
2344     my $e = new_editor();
2345     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2346     return $evt if $evt;
2347     return $cn;
2348 }
2349
2350
2351 __PACKAGE__->register_method(
2352     method    => "fetch_copy_by_cn",
2353     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2354     signature => q/
2355         Returns an array of copy ID's by callnumber ID
2356         @param cnid The callnumber ID
2357         @return An array of copy IDs
2358     /
2359 );
2360
2361 sub fetch_copy_by_cn {
2362     my( $self, $conn, $cnid ) = @_;
2363     return $U->cstorereq(
2364         'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2365         { call_number => $cnid, deleted => 'f' } );
2366 }
2367
2368 __PACKAGE__->register_method(
2369     method    => 'fetch_cn_by_info',
2370     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2371     signature => q/
2372         @param label The callnumber label
2373         @param record The record the cn is attached to
2374         @param org The owning library of the cn
2375         @return The callnumber object
2376     /
2377 );
2378
2379
2380 sub fetch_cn_by_info {
2381     my( $self, $conn, $label, $record, $org ) = @_;
2382     return $U->cstorereq(
2383         'open-ils.cstore.direct.asset.call_number.search',
2384         { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2385 }
2386
2387
2388
2389 __PACKAGE__->register_method(
2390     method   => 'bib_extras',
2391     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2392     ctype => 'lit_form'
2393 );
2394 __PACKAGE__->register_method(
2395     method   => 'bib_extras',
2396     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2397     ctype => 'item_form'
2398 );
2399 __PACKAGE__->register_method(
2400     method   => 'bib_extras',
2401     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2402     ctype => 'item_type',
2403 );
2404 __PACKAGE__->register_method(
2405     method   => 'bib_extras',
2406     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2407     ctype => 'bib_level'
2408 );
2409 __PACKAGE__->register_method(
2410     method   => 'bib_extras',
2411     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2412     ctype => 'audience'
2413 );
2414
2415 sub bib_extras {
2416     my $self = shift;
2417     $logger->warn("deprecation warning: " .$self->api_name);
2418
2419     my $e = new_editor();
2420
2421     my $ctype = $self->{ctype};
2422     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2423
2424     my @objs;
2425     for my $ccvm (@$ccvms) {
2426         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2427         $obj->value($ccvm->value);
2428         $obj->code($ccvm->code);
2429         $obj->description($ccvm->description) if $obj->can('description');
2430         push(@objs, $obj);
2431     }
2432
2433     return \@objs;
2434 }
2435
2436
2437
2438 __PACKAGE__->register_method(
2439     method    => 'fetch_slim_record',
2440     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2441     signature => {
2442         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2443         params => [
2444             { desc => 'Array of Record IDs', type => 'array' }
2445         ],
2446         return => { 
2447             desc => 'Array of biblio records, event on error'
2448         }
2449     }
2450 );
2451
2452 sub fetch_slim_record {
2453     my( $self, $conn, $ids ) = @_;
2454
2455     my $editor = new_editor();
2456     my @res;
2457     for( @$ids ) {
2458         return $editor->event unless
2459             my $r = $editor->retrieve_biblio_record_entry($_);
2460         $r->clear_marc;
2461         push(@res, $r);
2462     }
2463     return \@res;
2464 }
2465
2466 __PACKAGE__->register_method(
2467     method    => 'rec_hold_parts',
2468     api_name  => 'open-ils.search.biblio.record_hold_parts',
2469     signature => q/
2470        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2471     /
2472 );
2473
2474 sub rec_hold_parts {
2475     my( $self, $conn, $args ) = @_;
2476
2477     my $rec        = $$args{record};
2478     my $mrec       = $$args{metarecord};
2479     my $pickup_lib = $$args{pickup_lib};
2480     my $e = new_editor();
2481
2482     my $query = {
2483         select => {bmp => ['id', 'label']},
2484         from => 'bmp',
2485         where => {
2486             id => {
2487                 in => {
2488                     select => {'acpm' => ['part']},
2489                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2490                     where => {
2491                         '+acp' => {'deleted' => 'f'},
2492                         '+bre' => {id => $rec}
2493                     },
2494                     distinct => 1,
2495                 }
2496             },
2497             deleted => 'f'
2498         },
2499         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2500     };
2501
2502     if(defined $pickup_lib) {
2503         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2504         if($hard_boundary) {
2505             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2506             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2507         }
2508     }
2509
2510     return $e->json_query($query);
2511 }
2512
2513
2514
2515
2516 __PACKAGE__->register_method(
2517     method    => 'rec_to_mr_rec_descriptors',
2518     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2519     signature => q/
2520         specialized method...
2521         Given a biblio record id or a metarecord id, 
2522         this returns a list of metabib.record_descriptor
2523         objects that live within the same metarecord
2524         @param args Object of args including:
2525     /
2526 );
2527
2528 sub rec_to_mr_rec_descriptors {
2529     my( $self, $conn, $args ) = @_;
2530
2531     my $rec        = $$args{record};
2532     my $mrec       = $$args{metarecord};
2533     my $item_forms = $$args{item_forms};
2534     my $item_types = $$args{item_types};
2535     my $item_lang  = $$args{item_lang};
2536     my $pickup_lib = $$args{pickup_lib};
2537
2538     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2539
2540     my $e = new_editor();
2541     my $recs;
2542
2543     if( !$mrec ) {
2544         my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2545         return $e->event unless @$map;
2546         $mrec = $$map[0]->metarecord;
2547     }
2548
2549     $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2550     return $e->event unless @$recs;
2551
2552     my @recs = map { $_->source } @$recs;
2553     my $search = { record => \@recs };
2554     $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2555     $search->{item_type} = $item_types if $item_types and @$item_types;
2556     $search->{item_lang} = $item_lang  if $item_lang;
2557
2558     my $desc = $e->search_metabib_record_descriptor($search);
2559
2560     my $query = {
2561         distinct => 1,
2562         select   => { 'bre' => ['id'] },
2563         from     => {
2564             'bre' => {
2565                 'acn' => {
2566                     'join' => {
2567                         'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2568                       }
2569                   }
2570              }
2571         },
2572         where => {
2573             '+bre' => { id => \@recs },
2574             '+acp' => {
2575                 holdable => 't',
2576                 deleted  => 'f'
2577             },
2578             "+ccs" => { holdable => 't' },
2579             "+acpl" => { holdable => 't', deleted => 'f' }
2580         }
2581     };
2582
2583     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2584         my $orgs = $e->json_query(
2585             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2586         ) or return $e->die_event;
2587
2588         $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2589     }
2590
2591     my $good_records = $e->json_query($query) or return $e->die_event;
2592
2593     my @keep;
2594     for my $d (@$desc) {
2595         if ( grep { $d->record == $_->{id} } @$good_records ) {
2596             push @keep, $d;
2597         }
2598     }
2599
2600     $desc = \@keep;
2601
2602     return { metarecord => $mrec, descriptors => $desc };
2603 }
2604
2605
2606 __PACKAGE__->register_method(
2607     method   => 'fetch_age_protect',
2608     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2609 );
2610
2611 sub fetch_age_protect {
2612     return new_editor()->retrieve_all_config_rule_age_hold_protect();
2613 }
2614
2615
2616 __PACKAGE__->register_method(
2617     method   => 'copies_by_cn_label',
2618     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2619 );
2620
2621 __PACKAGE__->register_method(
2622     method   => 'copies_by_cn_label',
2623     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2624 );
2625
2626 sub copies_by_cn_label {
2627     my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2628     my $e = new_editor();
2629     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2630     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2631     my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2632     return [] unless @$cns;
2633
2634     # show all non-deleted copies in the staff client ...
2635     if ($self->api_name =~ /staff$/o) {
2636         return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2637     }
2638
2639     # ... otherwise, grab the copies ...
2640     my $copies = $e->search_asset_copy(
2641         [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2642           {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2643         ]
2644     );
2645
2646     # ... and test for location and status visibility
2647     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2648 }
2649
2650
2651 1;
2652