]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
LP#1698206: Eliminate Staged Search
[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 $est_hit_count = 0;
1208     my $current_page_summary = {};
1209     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1210     my $is_real_hit_count = 0;
1211     my $new_ids = [];
1212
1213     for($page = 0; $page < $max_superpages; $page++) {
1214
1215         my $data = $cache_data->{$page};
1216         my $results;
1217         my $summary;
1218
1219         $logger->debug("staged search: analyzing superpage $page");
1220
1221         if($data) {
1222             # this window of results is already cached
1223             $logger->debug("staged search: found cached results");
1224             $summary = $data->{summary};
1225             $results = $data->{results};
1226
1227         } else {
1228             # retrieve the window of results from the database
1229             $logger->debug("staged search: fetching results from the database");
1230             $search_hash->{skip_check} = $page * $superpage_size;
1231             $search_hash->{return_query} = $page == 0 ? 1 : 0;
1232             my $start = time;
1233             $results = $U->storagereq($method, %$search_hash);
1234             $search_duration = time - $start;
1235             $summary = shift(@$results) if $results;
1236
1237             unless($summary) {
1238                 $logger->info("search timed out: duration=$search_duration: params=".
1239                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1240                 return {count => 0};
1241             }
1242
1243             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1244
1245             my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1246             if($hc == 0) {
1247                 $logger->info("search returned 0 results: duration=$search_duration: params=".
1248                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1249             }
1250
1251             # Create backwards-compatible result structures
1252             if($IAmMetabib) {
1253                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1254             } else {
1255                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1256             }
1257
1258             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1259             $results = [grep {defined $_->[0]} @$results];
1260             cache_staged_search_page($key, $page, $summary, $results) if $docache;
1261         }
1262
1263         tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib) 
1264             if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1265
1266         $current_page_summary = $summary;
1267
1268         # add the new set of results to the set under construction
1269         push(@$all_results, @$results);
1270
1271         my $current_count = scalar(@$all_results);
1272
1273         if ($page == 0) {
1274             foreach (qw/ query_struct canonicalized_query /) {
1275                 $global_summary->{$_} = $summary->{$_};
1276             }
1277         }
1278
1279         $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1280             if $page == 0;
1281
1282         $logger->debug("staged search: located $current_count, with estimated hits=".
1283             ($summary->{estimated_hit_count} || "none") .
1284             " : visible=" . ($summary->{visible} || "none") . ", checked=" .
1285             ($summary->{checked} || "none")
1286         );
1287
1288         if (defined($summary->{estimated_hit_count})) {
1289             foreach (qw/ checked visible excluded deleted /) {
1290                 $global_summary->{$_} += $summary->{$_};
1291             }
1292             $global_summary->{total} = $summary->{total};
1293         }
1294
1295         # we've found all the possible hits
1296         last if $current_count == $summary->{visible}
1297             and not defined $summary->{estimated_hit_count};
1298
1299         # we've found enough results to satisfy the requested limit/offset
1300         last if $current_count >= ($user_limit + $user_offset);
1301
1302         # we've scanned all possible hits
1303         if($summary->{checked} < $superpage_size) {
1304             $est_hit_count = scalar(@$all_results);
1305             # we have all possible results in hand, so we know the final hit count
1306             $is_real_hit_count = 1;
1307             last;
1308         }
1309     }
1310
1311     # Let other backends grab our data now that we're done.
1312     $cache_data = $cache->get_cache($key);
1313     if ($$cache_data{running} and $$cache_data{running} == $$) {
1314         delete $$cache_data{running};
1315         $cache->put_cache($key, $cache_data, $cache_timeout);
1316     }
1317
1318     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1319
1320     # refine the estimate if we have more than one superpage
1321     if ($page > 0 and not $is_real_hit_count) {
1322         if ($global_summary->{checked} >= $global_summary->{total}) {
1323             $est_hit_count = $global_summary->{visible};
1324         } else {
1325             my $updated_hit_count = $U->storagereq(
1326                 'open-ils.storage.fts_paging_estimate',
1327                 $global_summary->{checked},
1328                 $global_summary->{visible},
1329                 $global_summary->{excluded},
1330                 $global_summary->{deleted},
1331                 $global_summary->{total}
1332             );
1333             $est_hit_count = $updated_hit_count->{$estimation_strategy};
1334         }
1335     }
1336
1337     $conn->respond_complete(
1338         {
1339             global_summary    => $global_summary,
1340             count             => $est_hit_count,
1341             core_limit        => $search_hash->{core_limit},
1342             superpage_size    => $search_hash->{check_limit},
1343             superpage_summary => $current_page_summary,
1344             facet_key         => $facet_key,
1345             ids               => \@results
1346         }
1347     );
1348
1349     $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1350
1351     return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1352 }
1353
1354 sub tag_circulated_records {
1355     my ($auth, $results, $metabib) = @_;
1356     my $e = new_editor(authtoken => $auth);
1357     return $results unless $e->checkauth;
1358
1359     my $query = {
1360         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1361         from     => { auch => { acp => { join => 'acn' }} }, 
1362         where    => { usr => $e->requestor->id },
1363         distinct => 1
1364     };
1365
1366     if ($metabib) {
1367         $query = {
1368             select   => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1369             from     => 'mmrsm',
1370             where    => { source => { in => $query } },
1371             distinct => 1
1372         };
1373     }
1374
1375     # Give me the distinct set of bib records that exist in the user's visible circulation history
1376     my $circ_recs = $e->json_query( $query );
1377
1378     # if the record appears in the circ history, push a 1 onto 
1379     # the rec array structure to indicate truthiness
1380     for my $rec (@$results) {
1381         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1382     }
1383
1384     $results
1385 }
1386
1387 # creates a unique token to represent the query in the cache
1388 sub search_cache_key {
1389     my $method = shift;
1390     my $search_hash = shift;
1391     my @sorted;
1392     for my $key (sort keys %$search_hash) {
1393         push(@sorted, ($key => $$search_hash{$key})) 
1394             unless $key eq 'limit'  or 
1395                    $key eq 'offset' or 
1396                    $key eq 'skip_check';
1397     }
1398     my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1399     return $pfx . md5_hex($method . $s);
1400 }
1401
1402 sub retrieve_cached_facets {
1403     my $self   = shift;
1404     my $client = shift;
1405     my $key    = shift;
1406     my $limit    = shift;
1407
1408     return undef unless ($key and $key =~ /_facets$/);
1409
1410     eval {
1411         local $SIG{ALRM} = sub {die};
1412         alarm(4); # we'll sleep for as much as 4s
1413         do {
1414             die if $cache->get_cache($key . '_COMPLETE');
1415         } while (sleep(0.05));
1416     };
1417
1418     my $blob = $cache->get_cache($key) || {};
1419
1420     my $facets = {};
1421     if ($limit) {
1422        for my $f ( keys %$blob ) {
1423             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1424             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1425             for my $s ( @sorted ) {
1426                 my ($k) = keys(%$s);
1427                 my ($v) = values(%$s);
1428                 $$facets{$f}{$k} = $v;
1429             }
1430         }
1431     } else {
1432         $facets = $blob;
1433     }
1434
1435     return $facets;
1436 }
1437
1438 __PACKAGE__->register_method(
1439     method   => "retrieve_cached_facets",
1440     api_name => "open-ils.search.facet_cache.retrieve",
1441     signature => {
1442         desc   => 'Returns facet data derived from a specific search based on a key '.
1443                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1444         params => [
1445             {
1446                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1447                 type => 'string',
1448             }
1449         ],
1450         return => {
1451             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1452                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1453                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1454                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1455             type => 'object',
1456         }
1457     }
1458 );
1459
1460
1461 sub cache_facets {
1462     # add facets for this search to the facet cache
1463     my($key, $results, $metabib, $ignore) = @_;
1464     my $data = $cache->get_cache($key);
1465     $data ||= {};
1466
1467     return undef unless (@$results);
1468
1469     my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1470                                    : 'search.facets_for_record_set';
1471     my $results_str = '{' . join(',', @$results) . '}';
1472     my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1473                                   : '{}';
1474     my $query = {   
1475         from => [ $facets_function, $ignore_str, $results_str ]
1476     };
1477
1478     my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1479
1480     for my $facet (@$facets) {
1481         next unless ($facet->{value});
1482         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1483     }
1484
1485     $logger->info("facet compilation: cached with key=$key");
1486
1487     $cache->put_cache($key, $data, $cache_timeout);
1488     $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1489 }
1490
1491 sub cache_staged_search_page {
1492     # puts this set of results into the cache
1493     my($key, $page, $summary, $results) = @_;
1494     my $data = $cache->get_cache($key);
1495     $data ||= {};
1496     $data->{$page} = {
1497         summary => $summary,
1498         results => $results
1499     };
1500
1501     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1502         ($summary->{estimated_hit_count} || "none") .
1503         ", visible=" . ($summary->{visible} || "none")
1504     );
1505
1506     $cache->put_cache($key, $data, $cache_timeout);
1507 }
1508
1509 sub search_cache {
1510
1511     my $key     = shift;
1512     my $offset  = shift;
1513     my $limit   = shift;
1514     my $start   = $offset;
1515     my $end     = $offset + $limit - 1;
1516
1517     $logger->debug("searching cache for $key : $start..$end\n");
1518
1519     return undef unless $cache;
1520     my $data = $cache->get_cache($key);
1521
1522     return undef unless $data;
1523
1524     my $count = $data->[0];
1525     $data = $data->[1];
1526
1527     return undef unless $offset < $count;
1528
1529     my @result;
1530     for( my $i = $offset; $i <= $end; $i++ ) {
1531         last unless my $d = $$data[$i];
1532         push( @result, $d );
1533     }
1534
1535     $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1536
1537     return \@result;
1538 }
1539
1540
1541 sub put_cache {
1542     my( $key, $count, $data ) = @_;
1543     return undef unless $cache;
1544     $logger->debug("search_cache putting ".
1545         scalar(@$data)." items at key $key with timeout $cache_timeout");
1546     $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1547 }
1548
1549
1550 __PACKAGE__->register_method(
1551     method   => "biblio_mrid_to_modsbatch_batch",
1552     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1553 );
1554
1555 sub biblio_mrid_to_modsbatch_batch {
1556     my( $self, $client, $mrids) = @_;
1557     # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1558     my @mods;
1559     my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1560     for my $id (@$mrids) {
1561         next unless defined $id;
1562         my ($m) = $method->run($id);
1563         push @mods, $m;
1564     }
1565     return \@mods;
1566 }
1567
1568
1569 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1570              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1571     {
1572     __PACKAGE__->register_method(
1573         method    => "biblio_mrid_to_modsbatch",
1574         api_name  => $_,
1575         signature => {
1576             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1577                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1578             params => [
1579                 { desc => 'Metarecord ID', type => 'number' },
1580                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1581             ],
1582             return => {
1583                 desc => 'MVR Object, event on error',
1584             }
1585         }
1586     );
1587 }
1588
1589 sub biblio_mrid_to_modsbatch {
1590     my( $self, $client, $mrid, $args) = @_;
1591
1592     # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1593
1594     my ($mr, $evt) = _grab_metarecord($mrid);
1595     return $evt unless $mr;
1596
1597     my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1598               biblio_mrid_make_modsbatch($self, $client, $mr);
1599
1600     return $mvr unless ref($args);  
1601
1602     # Here we find the lead record appropriate for the given filters 
1603     # and use that for the title and author of the metarecord
1604     my $format = $$args{format};
1605     my $org    = $$args{org};
1606     my $depth  = $$args{depth};
1607
1608     return $mvr unless $format or $org or $depth;
1609
1610     my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1611     $method = "$method.staff" if $self->api_name =~ /staff/o; 
1612
1613     my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1614
1615     if( my $mods = $U->record_to_mvr($rec) ) {
1616
1617         $mvr->title( $mods->title );
1618         $mvr->author($mods->author);
1619         $logger->debug("mods_slim updating title and ".
1620             "author in mvr with ".$mods->title." : ".$mods->author);
1621     }
1622
1623     return $mvr;
1624 }
1625
1626 # converts a metarecord to an mvr
1627 sub _mr_to_mvr {
1628     my $mr = shift;
1629     my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1630     return Fieldmapper::metabib::virtual_record->new($perl);
1631 }
1632
1633 # checks to see if a metarecord has mods, if so returns true;
1634
1635 __PACKAGE__->register_method(
1636     method   => "biblio_mrid_check_mvr",
1637     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1638     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1639               . "if the metarecord already has an mvr associated with it."
1640 );
1641
1642 sub biblio_mrid_check_mvr {
1643     my( $self, $client, $mrid ) = @_;
1644     my $mr; 
1645
1646     my $evt;
1647     if(ref($mrid)) { $mr = $mrid; } 
1648     else { ($mr, $evt) = _grab_metarecord($mrid); }
1649     return $evt if $evt;
1650
1651     # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1652
1653     return _mr_to_mvr($mr) if $mr->mods();
1654     return undef;
1655 }
1656
1657 sub _grab_metarecord {
1658     my $mrid = shift;
1659     my $e = new_editor();
1660     my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1661     return ($mr);
1662 }
1663
1664
1665 __PACKAGE__->register_method(
1666     method   => "biblio_mrid_make_modsbatch",
1667     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1668     notes    => "Takes either a metarecord ID or a metarecord object. "
1669               . "Forces the creations of an mvr for the given metarecord. "
1670               . "The created mvr is returned."
1671 );
1672
1673 sub biblio_mrid_make_modsbatch {
1674     my( $self, $client, $mrid ) = @_;
1675
1676     my $e = new_editor();
1677
1678     my $mr;
1679     if( ref($mrid) ) {
1680         $mr = $mrid;
1681         $mrid = $mr->id;
1682     } else {
1683         $mr = $e->retrieve_metabib_metarecord($mrid) 
1684             or return $e->event;
1685     }
1686
1687     my $masterid = $mr->master_record;
1688     $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1689
1690     my $ids = $U->storagereq(
1691         'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1692     return undef unless @$ids;
1693
1694     my $master = $e->retrieve_biblio_record_entry($masterid)
1695         or return $e->event;
1696
1697     # start the mods batch
1698     my $u = OpenILS::Utils::ModsParser->new();
1699     $u->start_mods_batch( $master->marc );
1700
1701     # grab all of the sub-records and shove them into the batch
1702     my @ids = grep { $_ ne $masterid } @$ids;
1703     #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1704
1705     my $subrecs = [];
1706     if(@$ids) {
1707         for my $i (@$ids) {
1708             my $r = $e->retrieve_biblio_record_entry($i);
1709             push( @$subrecs, $r ) if $r;
1710         }
1711     }
1712
1713     for(@$subrecs) {
1714         $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1715         $u->push_mods_batch( $_->marc ) if $_->marc;
1716     }
1717
1718
1719     # finish up and send to the client
1720     my $mods = $u->finish_mods_batch();
1721     $mods->doc_id($mrid);
1722     $client->respond_complete($mods);
1723
1724
1725     # now update the mods string in the db
1726     my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1727     $mr->mods($string);
1728
1729     $e = new_editor(xact => 1);
1730     $e->update_metabib_metarecord($mr) 
1731         or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1732     $e->finish;
1733
1734     return undef;
1735 }
1736
1737
1738 # converts a mr id into a list of record ids
1739
1740 foreach (qw/open-ils.search.biblio.metarecord_to_records
1741             open-ils.search.biblio.metarecord_to_records.staff/)
1742 {
1743     __PACKAGE__->register_method(
1744         method    => "biblio_mrid_to_record_ids",
1745         api_name  => $_,
1746         signature => {
1747             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1748                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1749             params => [
1750                 { desc => 'Metarecord ID', type => 'number' },
1751                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1752             ],
1753             return => {
1754                 desc => 'Results object like {count => $i, ids =>[...]}',
1755                 type => 'object'
1756             }
1757             
1758         }
1759     );
1760 }
1761
1762 sub biblio_mrid_to_record_ids {
1763     my( $self, $client, $mrid, $args ) = @_;
1764
1765     my $format = $$args{format};
1766     my $org    = $$args{org};
1767     my $depth  = $$args{depth};
1768
1769     my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1770     $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1771     my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1772
1773     return { count => scalar(@$recs), ids => $recs };
1774 }
1775
1776
1777 __PACKAGE__->register_method(
1778     method   => "biblio_record_to_marc_html",
1779     api_name => "open-ils.search.biblio.record.html"
1780 );
1781
1782 __PACKAGE__->register_method(
1783     method   => "biblio_record_to_marc_html",
1784     api_name => "open-ils.search.authority.to_html"
1785 );
1786
1787 # Persistent parsers and setting objects
1788 my $parser = XML::LibXML->new();
1789 my $xslt   = XML::LibXSLT->new();
1790 my $marc_sheet;
1791 my $slim_marc_sheet;
1792 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1793
1794 sub biblio_record_to_marc_html {
1795     my($self, $client, $recordid, $slim, $marcxml) = @_;
1796
1797     my $sheet;
1798     my $dir = $settings_client->config_value("dirs", "xsl");
1799
1800     if($slim) {
1801         unless($slim_marc_sheet) {
1802             my $xsl = $settings_client->config_value(
1803                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1804             if($xsl) {
1805                 $xsl = $parser->parse_file("$dir/$xsl");
1806                 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1807             }
1808         }
1809         $sheet = $slim_marc_sheet;
1810     }
1811
1812     unless($sheet) {
1813         unless($marc_sheet) {
1814             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1815             my $xsl = $settings_client->config_value(
1816                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1817             $xsl = $parser->parse_file("$dir/$xsl");
1818             $marc_sheet = $xslt->parse_stylesheet($xsl);
1819         }
1820         $sheet = $marc_sheet;
1821     }
1822
1823     my $record;
1824     unless($marcxml) {
1825         my $e = new_editor();
1826         if($self->api_name =~ /authority/) {
1827             $record = $e->retrieve_authority_record_entry($recordid)
1828                 or return $e->event;
1829         } else {
1830             $record = $e->retrieve_biblio_record_entry($recordid)
1831                 or return $e->event;
1832         }
1833         $marcxml = $record->marc;
1834     }
1835
1836     my $xmldoc = $parser->parse_string($marcxml);
1837     my $html = $sheet->transform($xmldoc);
1838     return $html->documentElement->toString();
1839 }
1840
1841 __PACKAGE__->register_method(
1842     method    => "format_biblio_record_entry",
1843     api_name  => "open-ils.search.biblio.record.print",
1844     signature => {
1845         desc   => 'Returns a printable version of the specified bib record',
1846         params => [
1847             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1848         ],
1849         return => {
1850             desc => q/An action_trigger.event object or error event./,
1851             type => 'object',
1852         }
1853     }
1854 );
1855 __PACKAGE__->register_method(
1856     method    => "format_biblio_record_entry",
1857     api_name  => "open-ils.search.biblio.record.email",
1858     signature => {
1859         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1860         params => [
1861             { desc => 'Authentication token',  type => 'string'},
1862             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1863         ],
1864         return => {
1865             desc => q/Undefined on success, otherwise an error event./,
1866             type => 'object',
1867         }
1868     }
1869 );
1870
1871 sub format_biblio_record_entry {
1872     my($self, $conn, $arg1, $arg2) = @_;
1873
1874     my $for_print = ($self->api_name =~ /print/);
1875     my $for_email = ($self->api_name =~ /email/);
1876
1877     my $e; my $auth; my $bib_id; my $context_org;
1878
1879     if ($for_print) {
1880         $bib_id = $arg1;
1881         $context_org = $arg2 || $U->get_org_tree->id;
1882         $e = new_editor(xact => 1);
1883     } elsif ($for_email) {
1884         $auth = $arg1;
1885         $bib_id = $arg2;
1886         $e = new_editor(authtoken => $auth, xact => 1);
1887         return $e->die_event unless $e->checkauth;
1888         $context_org = $e->requestor->home_ou;
1889     }
1890
1891     my $bib_ids;
1892     if (ref $bib_id ne 'ARRAY') {
1893         $bib_ids = [ $bib_id ];
1894     } else {
1895         $bib_ids = $bib_id;
1896     }
1897
1898     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1899     $bucket->btype('temp');
1900     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1901     if ($for_email) {
1902         $bucket->owner($e->requestor) 
1903     } else {
1904         $bucket->owner(1);
1905     }
1906     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1907
1908     for my $id (@$bib_ids) {
1909
1910         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1911
1912         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1913         $bucket_entry->target_biblio_record_entry($bib);
1914         $bucket_entry->bucket($bucket_obj->id);
1915         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1916     }
1917
1918     $e->commit;
1919
1920     if ($for_print) {
1921
1922         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1923
1924     } elsif ($for_email) {
1925
1926         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1927     }
1928
1929     return undef;
1930 }
1931
1932
1933 __PACKAGE__->register_method(
1934     method   => "retrieve_all_copy_statuses",
1935     api_name => "open-ils.search.config.copy_status.retrieve.all"
1936 );
1937
1938 sub retrieve_all_copy_statuses {
1939     my( $self, $client ) = @_;
1940     return new_editor()->retrieve_all_config_copy_status();
1941 }
1942
1943
1944 __PACKAGE__->register_method(
1945     method   => "copy_counts_per_org",
1946     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1947 );
1948
1949 __PACKAGE__->register_method(
1950     method   => "copy_counts_per_org",
1951     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1952 );
1953
1954 sub copy_counts_per_org {
1955     my( $self, $client, $record_id ) = @_;
1956
1957     warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1958
1959     my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1960     if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1961
1962     my $counts = $apputils->simple_scalar_request(
1963         "open-ils.storage", $method, $record_id );
1964
1965     $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1966     return $counts;
1967 }
1968
1969
1970 __PACKAGE__->register_method(
1971     method   => "copy_count_summary",
1972     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1973     notes    => "returns an array of these: "
1974               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1975               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1976 );
1977         
1978
1979 sub copy_count_summary {
1980     my( $self, $client, $rid, $org, $depth ) = @_;
1981     $org   ||= 1;
1982     $depth ||= 0;
1983     my $data = $U->storagereq(
1984         'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1985
1986     return [ sort {
1987         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
1988         cmp
1989         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
1990     } @$data ];
1991 }
1992
1993 __PACKAGE__->register_method(
1994     method   => "copy_location_count_summary",
1995     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1996     notes    => "returns an array of these: "
1997               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
1998               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1999 );
2000
2001 sub copy_location_count_summary {
2002     my( $self, $client, $rid, $org, $depth ) = @_;
2003     $org   ||= 1;
2004     $depth ||= 0;
2005     my $data = $U->storagereq(
2006         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2007
2008     return [ sort {
2009         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2010         cmp
2011         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2012
2013         || $a->[4] cmp $b->[4]
2014     } @$data ];
2015 }
2016
2017 __PACKAGE__->register_method(
2018     method   => "copy_count_location_summary",
2019     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2020     notes    => "returns an array of these: "
2021               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2022               . "where statusx is a copy status name.  The statuses are sorted by ID."
2023 );
2024
2025 sub copy_count_location_summary {
2026     my( $self, $client, $rid, $org, $depth ) = @_;
2027     $org   ||= 1;
2028     $depth ||= 0;
2029     my $data = $U->storagereq(
2030         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2031     return [ sort {
2032         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2033         cmp
2034         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2035     } @$data ];
2036 }
2037
2038
2039 foreach (qw/open-ils.search.biblio.marc
2040             open-ils.search.biblio.marc.staff/)
2041 {
2042 __PACKAGE__->register_method(
2043     method    => "marc_search",
2044     api_name  => $_,
2045     signature => {
2046         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2047                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2048         params => [
2049             {
2050                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2051                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2052                 type => 'object'
2053             },
2054             {desc => 'timeout (optional)',  type => 'number'}
2055         ],
2056         return => {
2057             desc => 'Results object like: { "count": $i, "ids": [...] }',
2058             type => 'object'
2059         }
2060     }
2061 );
2062 }
2063
2064 =head3 open-ils.search.biblio.marc (arghash, timeout)
2065
2066 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2067
2068     searches: complex query object  (required)
2069     org_unit: The org ID to focus the search at
2070     depth   : The org depth     
2071     limit   : integer search limit      default: 10
2072     offset  : integer search offset     default:  0
2073     sort    : What field to sort the results on? [ author | title | pubdate ]
2074     sort_dir: In what direction do we sort? [ asc | desc ]
2075
2076 Additional keys to refine search criteria:
2077
2078     audience : Audience
2079     language : Language (code)
2080     lit_form : Literary form
2081     item_form: Item form
2082     item_type: Item type
2083     format   : The MARC format
2084
2085 Please note that the specific strings to be used in the "addtional keys" will be entirely
2086 dependent on your loaded data.  
2087
2088 All keys except "searches" are optional.
2089 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2090
2091 For example, an arg hash might look like:
2092
2093     $arghash = {
2094         searches => [
2095             {
2096                 term     => "harry",
2097                 restrict => [
2098                     {
2099                         tag => 245,
2100                         subfield => "a"
2101                     }
2102                     # ...
2103                 ]
2104             }
2105             # ...
2106         ],
2107         org_unit  => 1,
2108         limit     => 5,
2109         sort      => "author",
2110         item_type => "g"
2111     }
2112
2113 The arghash is eventually passed to the SRF call:
2114 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2115
2116 Presently, search uses the cache unconditionally.
2117
2118 =cut
2119
2120 # FIXME: that example above isn't actually tested.
2121 # FIXME: sort and limit added.  item_type not tested yet.
2122 # TODO: docache option?
2123 sub marc_search {
2124     my( $self, $conn, $args, $timeout ) = @_;
2125
2126     my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2127     $method .= ".staff" if $self->api_name =~ /staff/;
2128     $method .= ".atomic";
2129
2130     my $limit = $args->{limit} || 10;
2131     my $offset = $args->{offset} || 0;
2132
2133     # allow caller to pass in a call timeout since MARC searches
2134     # can take longer than the default 60-second timeout.  
2135     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2136     $timeout = 120 if !$timeout or $timeout > 300;
2137
2138     my @search;
2139     push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2140     my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2141
2142     my $recs = search_cache($ckey, $offset, $limit);
2143
2144     if(!$recs) {
2145
2146         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2147         my $req = $ses->request($method, %$args);
2148         my $resp = $req->recv($timeout);
2149
2150         if($resp and $recs = $resp->content) {
2151             put_cache($ckey, scalar(@$recs), $recs);
2152         } else {
2153             $recs = [];
2154         }
2155
2156         $ses->kill_me;
2157     }
2158
2159     my $count = 0;
2160     $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2161     my @recs = map { $_->[0] } @$recs;
2162
2163     return { ids => \@recs, count => $count };
2164 }
2165
2166
2167 foreach my $isbn_method (qw/
2168     open-ils.search.biblio.isbn
2169     open-ils.search.biblio.isbn.staff
2170 /) {
2171 __PACKAGE__->register_method(
2172     method    => "biblio_search_isbn",
2173     api_name  => $isbn_method,
2174     signature => {
2175         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2176         params => [
2177             {desc => 'ISBN', type => 'string'}
2178         ],
2179         return => {
2180             desc => 'Results object like: { "count": $i, "ids": [...] }',
2181             type => 'object'
2182         }
2183     }
2184 );
2185 }
2186
2187 sub biblio_search_isbn { 
2188     my( $self, $client, $isbn ) = @_;
2189     $logger->debug("Searching ISBN $isbn");
2190     # the previous implementation of this method was essentially unlimited,
2191     # so we will set our limit very high and let multiclass.query provide any
2192     # actual limit
2193     # XXX: if making this unlimited is deemed important, we might consider
2194     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2195     # which is functionally deprecated at this point, or a custom call to
2196     # 'open-ils.storage.biblio.multiclass.search_fts'
2197
2198     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2199     if ($self->api_name =~ m/.staff$/) {
2200         $isbn_method .= '.staff';
2201     }
2202
2203     my $method = $self->method_lookup($isbn_method);
2204     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2205     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2206     return { ids => \@recs, count => $search_result->{'count'} };
2207 }
2208
2209 __PACKAGE__->register_method(
2210     method   => "biblio_search_isbn_batch",
2211     api_name => "open-ils.search.biblio.isbn_list",
2212 );
2213
2214 # XXX: see biblio_search_isbn() for note concerning 'limit'
2215 sub biblio_search_isbn_batch { 
2216     my( $self, $client, $isbn_list ) = @_;
2217     $logger->debug("Searching ISBNs @$isbn_list");
2218     my @recs = (); my %rec_set = ();
2219     my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2220     foreach my $isbn ( @$isbn_list ) {
2221         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2222         my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2223         foreach my $rec (@recs_subset) {
2224             if (! $rec_set{ $rec }) {
2225                 $rec_set{ $rec } = 1;
2226                 push @recs, $rec;
2227             }
2228         }
2229     }
2230     return { ids => \@recs, count => scalar(@recs) };
2231 }
2232
2233 foreach my $issn_method (qw/
2234     open-ils.search.biblio.issn
2235     open-ils.search.biblio.issn.staff
2236 /) {
2237 __PACKAGE__->register_method(
2238     method   => "biblio_search_issn",
2239     api_name => $issn_method,
2240     signature => {
2241         desc   => 'Retrieve biblio IDs for a given ISSN',
2242         params => [
2243             {desc => 'ISBN', type => 'string'}
2244         ],
2245         return => {
2246             desc => 'Results object like: { "count": $i, "ids": [...] }',
2247             type => 'object'
2248         }
2249     }
2250 );
2251 }
2252
2253 sub biblio_search_issn { 
2254     my( $self, $client, $issn ) = @_;
2255     $logger->debug("Searching ISSN $issn");
2256     # the previous implementation of this method was essentially unlimited,
2257     # so we will set our limit very high and let multiclass.query provide any
2258     # actual limit
2259     # XXX: if making this unlimited is deemed important, we might consider
2260     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2261     # which is functionally deprecated at this point, or a custom call to
2262     # 'open-ils.storage.biblio.multiclass.search_fts'
2263
2264     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2265     if ($self->api_name =~ m/.staff$/) {
2266         $issn_method .= '.staff';
2267     }
2268
2269     my $method = $self->method_lookup($issn_method);
2270     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2271     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2272     return { ids => \@recs, count => $search_result->{'count'} };
2273 }
2274
2275
2276 __PACKAGE__->register_method(
2277     method    => "fetch_mods_by_copy",
2278     api_name  => "open-ils.search.biblio.mods_from_copy",
2279     argc      => 1,
2280     signature => {
2281         desc    => 'Retrieve MODS record given an attached copy ID',
2282         params  => [
2283             { desc => 'Copy ID', type => 'number' }
2284         ],
2285         returns => {
2286             desc => 'MODS record, event on error or uncataloged item'
2287         }
2288     }
2289 );
2290
2291 sub fetch_mods_by_copy {
2292     my( $self, $client, $copyid ) = @_;
2293     my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2294     return $evt if $evt;
2295     return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2296     return $apputils->record_to_mvr($record);
2297 }
2298
2299
2300 # -------------------------------------------------------------------------------------
2301
2302 __PACKAGE__->register_method(
2303     method   => "cn_browse",
2304     api_name => "open-ils.search.callnumber.browse.target",
2305     notes    => "Starts a callnumber browse"
2306 );
2307
2308 __PACKAGE__->register_method(
2309     method   => "cn_browse",
2310     api_name => "open-ils.search.callnumber.browse.page_up",
2311     notes    => "Returns the previous page of callnumbers",
2312 );
2313
2314 __PACKAGE__->register_method(
2315     method   => "cn_browse",
2316     api_name => "open-ils.search.callnumber.browse.page_down",
2317     notes    => "Returns the next page of callnumbers",
2318 );
2319
2320
2321 # RETURNS array of arrays like so: label, owning_lib, record, id
2322 sub cn_browse {
2323     my( $self, $client, @params ) = @_;
2324     my $method;
2325
2326     $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2327         if( $self->api_name =~ /target/ );
2328     $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2329         if( $self->api_name =~ /page_up/ );
2330     $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2331         if( $self->api_name =~ /page_down/ );
2332
2333     return $apputils->simplereq( 'open-ils.storage', $method, @params );
2334 }
2335 # -------------------------------------------------------------------------------------
2336
2337 __PACKAGE__->register_method(
2338     method        => "fetch_cn",
2339     api_name      => "open-ils.search.callnumber.retrieve",
2340     authoritative => 1,
2341     notes         => "retrieves a callnumber based on ID",
2342 );
2343
2344 sub fetch_cn {
2345     my( $self, $client, $id ) = @_;
2346
2347     my $e = new_editor();
2348     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2349     return $evt if $evt;
2350     return $cn;
2351 }
2352
2353 __PACKAGE__->register_method(
2354     method        => "fetch_fleshed_cn",
2355     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2356     authoritative => 1,
2357     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2358 );
2359
2360 sub fetch_fleshed_cn {
2361     my( $self, $client, $id ) = @_;
2362
2363     my $e = new_editor();
2364     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2365     return $evt if $evt;
2366     return $cn;
2367 }
2368
2369
2370 __PACKAGE__->register_method(
2371     method    => "fetch_copy_by_cn",
2372     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2373     signature => q/
2374         Returns an array of copy ID's by callnumber ID
2375         @param cnid The callnumber ID
2376         @return An array of copy IDs
2377     /
2378 );
2379
2380 sub fetch_copy_by_cn {
2381     my( $self, $conn, $cnid ) = @_;
2382     return $U->cstorereq(
2383         'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2384         { call_number => $cnid, deleted => 'f' } );
2385 }
2386
2387 __PACKAGE__->register_method(
2388     method    => 'fetch_cn_by_info',
2389     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2390     signature => q/
2391         @param label The callnumber label
2392         @param record The record the cn is attached to
2393         @param org The owning library of the cn
2394         @return The callnumber object
2395     /
2396 );
2397
2398
2399 sub fetch_cn_by_info {
2400     my( $self, $conn, $label, $record, $org ) = @_;
2401     return $U->cstorereq(
2402         'open-ils.cstore.direct.asset.call_number.search',
2403         { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2404 }
2405
2406
2407
2408 __PACKAGE__->register_method(
2409     method   => 'bib_extras',
2410     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2411     ctype => 'lit_form'
2412 );
2413 __PACKAGE__->register_method(
2414     method   => 'bib_extras',
2415     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2416     ctype => 'item_form'
2417 );
2418 __PACKAGE__->register_method(
2419     method   => 'bib_extras',
2420     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2421     ctype => 'item_type',
2422 );
2423 __PACKAGE__->register_method(
2424     method   => 'bib_extras',
2425     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2426     ctype => 'bib_level'
2427 );
2428 __PACKAGE__->register_method(
2429     method   => 'bib_extras',
2430     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2431     ctype => 'audience'
2432 );
2433
2434 sub bib_extras {
2435     my $self = shift;
2436     $logger->warn("deprecation warning: " .$self->api_name);
2437
2438     my $e = new_editor();
2439
2440     my $ctype = $self->{ctype};
2441     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2442
2443     my @objs;
2444     for my $ccvm (@$ccvms) {
2445         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2446         $obj->value($ccvm->value);
2447         $obj->code($ccvm->code);
2448         $obj->description($ccvm->description) if $obj->can('description');
2449         push(@objs, $obj);
2450     }
2451
2452     return \@objs;
2453 }
2454
2455
2456
2457 __PACKAGE__->register_method(
2458     method    => 'fetch_slim_record',
2459     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2460     signature => {
2461         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2462         params => [
2463             { desc => 'Array of Record IDs', type => 'array' }
2464         ],
2465         return => { 
2466             desc => 'Array of biblio records, event on error'
2467         }
2468     }
2469 );
2470
2471 sub fetch_slim_record {
2472     my( $self, $conn, $ids ) = @_;
2473
2474     my $editor = new_editor();
2475     my @res;
2476     for( @$ids ) {
2477         return $editor->event unless
2478             my $r = $editor->retrieve_biblio_record_entry($_);
2479         $r->clear_marc;
2480         push(@res, $r);
2481     }
2482     return \@res;
2483 }
2484
2485 __PACKAGE__->register_method(
2486     method    => 'rec_hold_parts',
2487     api_name  => 'open-ils.search.biblio.record_hold_parts',
2488     signature => q/
2489        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2490     /
2491 );
2492
2493 sub rec_hold_parts {
2494     my( $self, $conn, $args ) = @_;
2495
2496     my $rec        = $$args{record};
2497     my $mrec       = $$args{metarecord};
2498     my $pickup_lib = $$args{pickup_lib};
2499     my $e = new_editor();
2500
2501     my $query = {
2502         select => {bmp => ['id', 'label']},
2503         from => 'bmp',
2504         where => {
2505             id => {
2506                 in => {
2507                     select => {'acpm' => ['part']},
2508                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2509                     where => {
2510                         '+acp' => {'deleted' => 'f'},
2511                         '+bre' => {id => $rec}
2512                     },
2513                     distinct => 1,
2514                 }
2515             },
2516             deleted => 'f'
2517         },
2518         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2519     };
2520
2521     if(defined $pickup_lib) {
2522         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2523         if($hard_boundary) {
2524             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2525             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2526         }
2527     }
2528
2529     return $e->json_query($query);
2530 }
2531
2532
2533
2534
2535 __PACKAGE__->register_method(
2536     method    => 'rec_to_mr_rec_descriptors',
2537     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2538     signature => q/
2539         specialized method...
2540         Given a biblio record id or a metarecord id, 
2541         this returns a list of metabib.record_descriptor
2542         objects that live within the same metarecord
2543         @param args Object of args including:
2544     /
2545 );
2546
2547 sub rec_to_mr_rec_descriptors {
2548     my( $self, $conn, $args ) = @_;
2549
2550     my $rec        = $$args{record};
2551     my $mrec       = $$args{metarecord};
2552     my $item_forms = $$args{item_forms};
2553     my $item_types = $$args{item_types};
2554     my $item_lang  = $$args{item_lang};
2555     my $pickup_lib = $$args{pickup_lib};
2556
2557     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2558
2559     my $e = new_editor();
2560     my $recs;
2561
2562     if( !$mrec ) {
2563         my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2564         return $e->event unless @$map;
2565         $mrec = $$map[0]->metarecord;
2566     }
2567
2568     $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2569     return $e->event unless @$recs;
2570
2571     my @recs = map { $_->source } @$recs;
2572     my $search = { record => \@recs };
2573     $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2574     $search->{item_type} = $item_types if $item_types and @$item_types;
2575     $search->{item_lang} = $item_lang  if $item_lang;
2576
2577     my $desc = $e->search_metabib_record_descriptor($search);
2578
2579     my $query = {
2580         distinct => 1,
2581         select   => { 'bre' => ['id'] },
2582         from     => {
2583             'bre' => {
2584                 'acn' => {
2585                     'join' => {
2586                         'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2587                       }
2588                   }
2589              }
2590         },
2591         where => {
2592             '+bre' => { id => \@recs },
2593             '+acp' => {
2594                 holdable => 't',
2595                 deleted  => 'f'
2596             },
2597             "+ccs" => { holdable => 't' },
2598             "+acpl" => { holdable => 't', deleted => 'f' }
2599         }
2600     };
2601
2602     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2603         my $orgs = $e->json_query(
2604             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2605         ) or return $e->die_event;
2606
2607         $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2608     }
2609
2610     my $good_records = $e->json_query($query) or return $e->die_event;
2611
2612     my @keep;
2613     for my $d (@$desc) {
2614         if ( grep { $d->record == $_->{id} } @$good_records ) {
2615             push @keep, $d;
2616         }
2617     }
2618
2619     $desc = \@keep;
2620
2621     return { metarecord => $mrec, descriptors => $desc };
2622 }
2623
2624
2625 __PACKAGE__->register_method(
2626     method   => 'fetch_age_protect',
2627     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2628 );
2629
2630 sub fetch_age_protect {
2631     return new_editor()->retrieve_all_config_rule_age_hold_protect();
2632 }
2633
2634
2635 __PACKAGE__->register_method(
2636     method   => 'copies_by_cn_label',
2637     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2638 );
2639
2640 __PACKAGE__->register_method(
2641     method   => 'copies_by_cn_label',
2642     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2643 );
2644
2645 sub copies_by_cn_label {
2646     my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2647     my $e = new_editor();
2648     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2649     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2650     my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2651     return [] unless @$cns;
2652
2653     # show all non-deleted copies in the staff client ...
2654     if ($self->api_name =~ /staff$/o) {
2655         return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2656     }
2657
2658     # ... otherwise, grab the copies ...
2659     my $copies = $e->search_asset_copy(
2660         [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2661           {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2662         ]
2663     );
2664
2665     # ... and test for location and status visibility
2666     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2667 }
2668
2669
2670 1;
2671