LP#1310283: Propogate .staff through Z3950 searches
[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);
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     my($self, $conn, $arghash, $query, $docache) = @_;
833
834     $logger->debug("initial search query => $query");
835     my $orig_query = $query;
836
837     $query =~ s/\+/ /go;
838     $query =~ s/^\s+//go;
839
840     # convert convenience classes (e.g. kw for keyword) to the full class name
841     # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
842     $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
843     $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
844     $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
845     $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
846     $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
847     $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
848
849     $logger->debug("cleansed query string => $query");
850     my $search = {};
851
852     my $simple_class_re  = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
853     my $class_list_re    = qr/(?:keyword|title|author|subject|series)/;
854     my $modifier_list_re = qr/(?:site|dir|sort|lang|available|preflib)/;
855
856     my $tmp_value = '';
857     while ($query =~ s/$simple_class_re//so) {
858
859         my $qpart = $1;
860         my $where = index($qpart,':');
861         my $type  = substr($qpart, 0, $where++);
862         my $value = substr($qpart, $where);
863
864         if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
865             $tmp_value = "$qpart $tmp_value";
866             next;
867         }
868
869         if ($type =~ /$class_list_re/o ) {
870             $value .= $tmp_value;
871             $tmp_value = '';
872         }
873
874         next unless $type and $value;
875
876         $value =~ s/^\s*//og;
877         $value =~ s/\s*$//og;
878         $type = 'sort_dir' if $type eq 'dir';
879
880         if($type eq 'site') {
881             # 'site' is the org shortname.  when using this, we also want 
882             # to search at the requested org's depth
883             my $e = new_editor();
884             if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
885                 $arghash->{org_unit} = $org->id if $org;
886                 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
887             } else {
888                 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
889             }
890         } elsif($type eq 'pref_ou') {
891             # 'pref_ou' is the preferred org shortname.
892             my $e = new_editor();
893             if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
894                 $arghash->{pref_ou} = $org->id if $org;
895             } else {
896                 $logger->warn("'pref_ou:' query used on invalid org shortname: $value ... ignoring");
897             }
898
899         } elsif($type eq 'available') {
900             # limit to available
901             $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
902
903         } elsif($type eq 'lang') {
904             # collect languages into an array of languages
905             $arghash->{language} = [] unless $arghash->{language};
906             push(@{$arghash->{language}}, $value);
907
908         } elsif($type =~ /^sort/o) {
909             # sort and sort_dir modifiers
910             $arghash->{$type} = $value;
911
912         } else {
913             # append the search term to the term under construction
914             $search->{$type} =  {} unless $search->{$type};
915             $search->{$type}->{term} =  
916                 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
917         }
918     }
919
920     $query .= " $tmp_value";
921     $query =~ s/\s+/ /go;
922     $query =~ s/^\s+//go;
923     $query =~ s/\s+$//go;
924
925     my $type = $arghash->{default_class} || 'keyword';
926     $type = ($type eq '-') ? 'keyword' : $type;
927     $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
928
929     if($query) {
930         # This is the front part of the string before any special tokens were
931         # parsed OR colon-separated strings that do not denote a class.
932         # Add this data to the default search class
933         $search->{$type} =  {} unless $search->{$type};
934         $search->{$type}->{term} =
935             ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
936     }
937     my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
938
939     # capture the original limit because the search method alters the limit internally
940     my $ol = $arghash->{limit};
941
942     my $sclient = OpenSRF::Utils::SettingsClient->new;
943
944     (my $method = $self->api_name) =~ s/\.query//o;
945
946     $method =~ s/multiclass/multiclass.staged/
947         if $sclient->config_value(apps => 'open-ils.search',
948             app_settings => 'use_staged_search') =~ /true/i;
949
950     # XXX This stops the session locale from doing the right thing.
951     # XXX Revisit this and have it translate to a lang instead of a locale.
952     #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
953     #    unless $arghash->{preferred_language};
954
955     $method = $self->method_lookup($method);
956     my ($data) = $method->run($arghash, $docache);
957
958     $arghash->{searches} = $search if (!$data->{complex_query});
959
960     $arghash->{limit} = $ol if $ol;
961     $data->{compiled_search} = $arghash;
962     $data->{query} = $orig_query;
963
964     $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
965
966     return $data;
967 }
968
969 __PACKAGE__->register_method(
970     method    => 'cat_search_z_style_wrapper',
971     api_name  => 'open-ils.search.biblio.zstyle',
972     stream    => 1,
973     signature => q/@see open-ils.search.biblio.multiclass/
974 );
975
976 __PACKAGE__->register_method(
977     method    => 'cat_search_z_style_wrapper',
978     api_name  => 'open-ils.search.biblio.zstyle.staff',
979     stream    => 1,
980     signature => q/@see open-ils.search.biblio.multiclass/
981 );
982
983 sub cat_search_z_style_wrapper {
984     my $self = shift;
985     my $client = shift;
986     my $authtoken = shift;
987     my $args = shift;
988
989     my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
990
991     my $ou = $cstore->request(
992         'open-ils.cstore.direct.actor.org_unit.search',
993         { parent_ou => undef }
994     )->gather(1);
995
996     my $result = { service => 'native-evergreen-catalog', records => [] };
997     my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
998
999     $$searchhash{searches}{title}{term}   = $$args{search}{title}   if $$args{search}{title};
1000     $$searchhash{searches}{author}{term}  = $$args{search}{author}  if $$args{search}{author};
1001     $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
1002     $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
1003     $$searchhash{searches}{'identifier|isbn'}{term} = $$args{search}{isbn} if $$args{search}{isbn};
1004     $$searchhash{searches}{'identifier|issn'}{term} = $$args{search}{issn} if $$args{search}{issn};
1005
1006     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn}       if $$args{search}{tcn};
1007     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
1008     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate}   if $$args{search}{pubdate};
1009     $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
1010
1011     my $method = 'open-ils.search.biblio.multiclass.staged';
1012     $method .= '.staff' if $self->api_name =~ /staff$/;
1013
1014     my ($list) = $self->method_lookup($method)->run( $searchhash );
1015
1016     if ($list->{count} > 0 and @{$list->{ids}}) {
1017         $result->{count} = $list->{count};
1018
1019         my $records = $cstore->request(
1020             'open-ils.cstore.direct.biblio.record_entry.search.atomic',
1021             { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
1022         )->gather(1);
1023
1024         for my $rec ( @$records ) {
1025             
1026             my $u = OpenILS::Utils::ModsParser->new();
1027                         $u->start_mods_batch( $rec->marc );
1028                         my $mods = $u->finish_mods_batch();
1029
1030             push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
1031
1032         }
1033
1034     }
1035
1036     $cstore->disconnect();
1037     return $result;
1038 }
1039
1040 # ----------------------------------------------------------------------------
1041 # These are the main OPAC search methods
1042 # ----------------------------------------------------------------------------
1043
1044 __PACKAGE__->register_method(
1045     method    => 'the_quest_for_knowledge',
1046     api_name  => 'open-ils.search.biblio.multiclass',
1047     signature => {
1048         desc => "Performs a multi class biblio or metabib search",
1049         params => [
1050             {
1051                 desc => "A search hash with keys: "
1052                       . "searches, org_unit, depth, limit, offset, format, sort, sort_dir.  "
1053                       . "See perldoc " . __PACKAGE__ . " for more detail",
1054                 type => 'object',
1055             },
1056             {
1057                 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
1058                 type => 'string',
1059             }
1060         ],
1061         return => {
1062             desc => 'An object of the form: '
1063                   . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
1064         }
1065     }
1066 );
1067
1068 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
1069
1070 The search-hash argument can have the following elements:
1071
1072     searches: { "$class" : "$value", ...}           [REQUIRED]
1073     org_unit: The org id to focus the search at
1074     depth   : The org depth     
1075     limit   : The search limit      default: 10
1076     offset  : The search offset     default:  0
1077     format  : The MARC format
1078     sort    : What field to sort the results on? [ author | title | pubdate ]
1079     sort_dir: What direction do we sort? [ asc | desc ]
1080     tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
1081         will be tagged with an additional value ("1") as the last value in the record ID array for
1082         each record.  Requires the 'authtoken'
1083     authtoken : Authentication token string;  When actions are performed that require a user login
1084         (e.g. tagging circulated records), the authentication token is required
1085
1086 The searches element is required, must have a hashref value, and the hashref must contain at least one 
1087 of the following classes as a key:
1088
1089     title
1090     author
1091     subject
1092     series
1093     keyword
1094
1095 The value paired with a key is the associated search string.
1096
1097 The docache argument enables/disables searching and saving results in cache (default OFF).
1098
1099 The return object, if successful, will look like:
1100
1101     { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
1102
1103 =cut
1104
1105 __PACKAGE__->register_method(
1106     method    => 'the_quest_for_knowledge',
1107     api_name  => 'open-ils.search.biblio.multiclass.staff',
1108     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
1109 );
1110 __PACKAGE__->register_method(
1111     method    => 'the_quest_for_knowledge',
1112     api_name  => 'open-ils.search.metabib.multiclass',
1113     signature => q/@see open-ils.search.biblio.multiclass/
1114 );
1115 __PACKAGE__->register_method(
1116     method    => 'the_quest_for_knowledge',
1117     api_name  => 'open-ils.search.metabib.multiclass.staff',
1118     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
1119 );
1120
1121 sub the_quest_for_knowledge {
1122     my( $self, $conn, $searchhash, $docache ) = @_;
1123
1124     return { count => 0 } unless $searchhash and
1125         ref $searchhash->{searches} eq 'HASH';
1126
1127     my $method = 'open-ils.storage.biblio.multiclass.search_fts';
1128     my $ismeta = 0;
1129     my @recs;
1130
1131     if($self->api_name =~ /metabib/) {
1132         $ismeta = 1;
1133         $method =~ s/biblio/metabib/o;
1134     }
1135
1136     # do some simple sanity checking
1137     if(!$searchhash->{searches} or
1138         ( !grep { /^(?:title|author|subject|series|keyword|identifier\|is[bs]n)/ } keys %{$searchhash->{searches}} ) ) {
1139         return { count => 0 };
1140     }
1141
1142     my $offset = $searchhash->{offset} ||  0;   # user value or default in local var now
1143     my $limit  = $searchhash->{limit}  || 10;   # user value or default in local var now
1144     my $end    = $offset + $limit - 1;
1145
1146     my $maxlimit = 5000;
1147     $searchhash->{offset} = 0;                  # possible user value overwritten in hash
1148     $searchhash->{limit}  = $maxlimit;          # possible user value overwritten in hash
1149
1150     return { count => 0 } if $offset > $maxlimit;
1151
1152     my @search;
1153     push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
1154     my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
1155     my $ckey = $pfx . md5_hex($method . $s);
1156
1157     $logger->info("bib search for: $s");
1158
1159     $searchhash->{limit} -= $offset;
1160
1161
1162     my $trim = 0;
1163     my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1164
1165     if(!$result) {
1166
1167         $method .= ".staff" if($self->api_name =~ /staff/);
1168         $method .= ".atomic";
1169     
1170         for (keys %$searchhash) { 
1171             delete $$searchhash{$_} 
1172                 unless defined $$searchhash{$_}; 
1173         }
1174     
1175         $result = $U->storagereq( $method, %$searchhash );
1176         $trim = 1;
1177
1178     } else { 
1179         $docache = 0;   # results came FROM cache, so we don't write back
1180     }
1181
1182     return {count => 0} unless ($result && $$result[0]);
1183
1184     @recs = @$result;
1185
1186     my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1187
1188     if($docache) {
1189         # If we didn't get this data from the cache, put it into the cache
1190         # then return the correct offset of records
1191         $logger->debug("putting search cache $ckey\n");
1192         put_cache($ckey, $count, \@recs);
1193     }
1194
1195     if($trim) {
1196         # if we have the full set of data, trim out 
1197         # the requested chunk based on limit and offset
1198         my @t;
1199         for ($offset..$end) {
1200             last unless $recs[$_];
1201             push(@t, $recs[$_]);
1202         }
1203         @recs = @t;
1204     }
1205
1206     return { ids => \@recs, count => $count };
1207 }
1208
1209
1210 __PACKAGE__->register_method(
1211     method    => 'staged_search',
1212     api_name  => 'open-ils.search.biblio.multiclass.staged',
1213     signature => {
1214         desc   => 'Staged search filters out unavailable items.  This means that it relies on an estimation strategy for determining ' .
1215                   'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering.  See "estimation_strategy" in your SRF config.',
1216         params => [
1217             {
1218                 desc => "A search hash with keys: "
1219                       . "searches, limit, offset.  The others are optional, but the 'searches' key/value pair is required, with the value being a hashref.  "
1220                       . "See perldoc " . __PACKAGE__ . " for more detail",
1221                 type => 'object',
1222             },
1223             {
1224                 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1225                 type => 'string',
1226             }
1227         ],
1228         return => {
1229             desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids.  '
1230                   . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1231             type => 'object',
1232         }
1233     }
1234 );
1235 __PACKAGE__->register_method(
1236     method    => 'staged_search',
1237     api_name  => 'open-ils.search.biblio.multiclass.staged.staff',
1238     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1239 );
1240 __PACKAGE__->register_method(
1241     method    => 'staged_search',
1242     api_name  => 'open-ils.search.metabib.multiclass.staged',
1243     signature => q/@see open-ils.search.biblio.multiclass.staged/
1244 );
1245 __PACKAGE__->register_method(
1246     method    => 'staged_search',
1247     api_name  => 'open-ils.search.metabib.multiclass.staged.staff',
1248     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1249 );
1250
1251 sub staged_search {
1252     my($self, $conn, $search_hash, $docache) = @_;
1253
1254     my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1255
1256     my $method = $IAmMetabib?
1257         'open-ils.storage.metabib.multiclass.staged.search_fts':
1258         'open-ils.storage.biblio.multiclass.staged.search_fts';
1259
1260     $method .= '.staff' if $self->api_name =~ /staff$/;
1261     $method .= '.atomic';
1262                 
1263     return {count => 0} unless (
1264         $search_hash and 
1265         $search_hash->{searches} and 
1266         scalar( keys %{$search_hash->{searches}} ));
1267
1268     my $search_duration;
1269     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
1270     my $user_limit  = $search_hash->{limit}  || 10;
1271     my $ignore_facet_classes  = $search_hash->{ignore_facet_classes};
1272     $user_offset = ($user_offset >= 0) ? $user_offset :  0;
1273     $user_limit  = ($user_limit  >= 0) ? $user_limit  : 10;
1274
1275
1276     # we're grabbing results on a per-superpage basis, which means the 
1277     # limit and offset should coincide with superpage boundaries
1278     $search_hash->{offset} = 0;
1279     $search_hash->{limit} = $superpage_size;
1280
1281     # force a well-known check_limit
1282     $search_hash->{check_limit} = $superpage_size; 
1283     # restrict total tested to superpage size * number of superpages
1284     $search_hash->{core_limit}  = $superpage_size * $max_superpages;
1285
1286     # Set the configured estimation strategy, defaults to 'inclusion'.
1287     my $estimation_strategy = OpenSRF::Utils::SettingsClient
1288         ->new
1289         ->config_value(
1290             apps => 'open-ils.search', app_settings => 'estimation_strategy'
1291         ) || 'inclusion';
1292     $search_hash->{estimation_strategy} = $estimation_strategy;
1293
1294     # pull any existing results from the cache
1295     my $key = search_cache_key($method, $search_hash);
1296     my $facet_key = $key.'_facets';
1297     my $cache_data = $cache->get_cache($key) || {};
1298
1299     # First, we want to make sure that someone else isn't currently trying to perform exactly
1300     # this same search.  The point is to allow just one instance of a search to fill the needs
1301     # of all concurrent, identical searches.  This will avoid spammy searches killing the
1302     # database without requiring admins to start locking some IP addresses out entirely.
1303     #
1304     # There's still a tiny race condition where 2 might run, but without sigificantly more code
1305     # and complexity, this is close to the best we can do.
1306
1307     if ($cache_data->{running}) { # someone is already doing the search...
1308         my $stop_looping = time() + $cache_timeout;
1309         while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1310             $cache_data = $cache->get_cache($key) || {};
1311             last if (!$cache_data->{running});
1312         }
1313     } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1314         $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1315     }
1316
1317     # keep retrieving results until we find enough to 
1318     # fulfill the user-specified limit and offset
1319     my $all_results = [];
1320     my $page; # current superpage
1321     my $est_hit_count = 0;
1322     my $current_page_summary = {};
1323     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1324     my $is_real_hit_count = 0;
1325     my $new_ids = [];
1326
1327     for($page = 0; $page < $max_superpages; $page++) {
1328
1329         my $data = $cache_data->{$page};
1330         my $results;
1331         my $summary;
1332
1333         $logger->debug("staged search: analyzing superpage $page");
1334
1335         if($data) {
1336             # this window of results is already cached
1337             $logger->debug("staged search: found cached results");
1338             $summary = $data->{summary};
1339             $results = $data->{results};
1340
1341         } else {
1342             # retrieve the window of results from the database
1343             $logger->debug("staged search: fetching results from the database");
1344             $search_hash->{skip_check} = $page * $superpage_size;
1345             my $start = time;
1346             $results = $U->storagereq($method, %$search_hash);
1347             $search_duration = time - $start;
1348             $summary = shift(@$results) if $results;
1349
1350             unless($summary) {
1351                 $logger->info("search timed out: duration=$search_duration: params=".
1352                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1353                 return {count => 0};
1354             }
1355
1356             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1357
1358             my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1359             if($hc == 0) {
1360                 $logger->info("search returned 0 results: duration=$search_duration: params=".
1361                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1362             }
1363
1364             # Create backwards-compatible result structures
1365             if($IAmMetabib) {
1366                 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1367             } else {
1368                 $results = [map {[$_->{id}]} @$results];
1369             }
1370
1371             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1372             $results = [grep {defined $_->[0]} @$results];
1373             cache_staged_search_page($key, $page, $summary, $results) if $docache;
1374         }
1375
1376         tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib) 
1377             if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1378
1379         $current_page_summary = $summary;
1380
1381         # add the new set of results to the set under construction
1382         push(@$all_results, @$results);
1383
1384         my $current_count = scalar(@$all_results);
1385
1386         $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1387             if $page == 0;
1388
1389         $logger->debug("staged search: located $current_count, with estimated hits=".
1390             ($summary->{estimated_hit_count} || "none") .
1391             " : visible=" . ($summary->{visible} || "none") . ", checked=" .
1392             ($summary->{checked} || "none")
1393         );
1394
1395         if (defined($summary->{estimated_hit_count})) {
1396             foreach (qw/ checked visible excluded deleted /) {
1397                 $global_summary->{$_} += $summary->{$_};
1398             }
1399             $global_summary->{total} = $summary->{total};
1400         }
1401
1402         # we've found all the possible hits
1403         last if $current_count == $summary->{visible}
1404             and not defined $summary->{estimated_hit_count};
1405
1406         # we've found enough results to satisfy the requested limit/offset
1407         last if $current_count >= ($user_limit + $user_offset);
1408
1409         # we've scanned all possible hits
1410         if($summary->{checked} < $superpage_size) {
1411             $est_hit_count = scalar(@$all_results);
1412             # we have all possible results in hand, so we know the final hit count
1413             $is_real_hit_count = 1;
1414             last;
1415         }
1416     }
1417
1418     # Let other backends grab our data now that we're done.
1419     $cache_data = $cache->get_cache($key);
1420     if ($$cache_data{running} and $$cache_data{running} == $$) {
1421         delete $$cache_data{running};
1422         $cache->put_cache($key, $cache_data, $cache_timeout);
1423     }
1424
1425     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1426
1427     # refine the estimate if we have more than one superpage
1428     if ($page > 0 and not $is_real_hit_count) {
1429         if ($global_summary->{checked} >= $global_summary->{total}) {
1430             $est_hit_count = $global_summary->{visible};
1431         } else {
1432             my $updated_hit_count = $U->storagereq(
1433                 'open-ils.storage.fts_paging_estimate',
1434                 $global_summary->{checked},
1435                 $global_summary->{visible},
1436                 $global_summary->{excluded},
1437                 $global_summary->{deleted},
1438                 $global_summary->{total}
1439             );
1440             $est_hit_count = $updated_hit_count->{$estimation_strategy};
1441         }
1442     }
1443
1444     $conn->respond_complete(
1445         {
1446             count             => $est_hit_count,
1447             core_limit        => $search_hash->{core_limit},
1448             superpage_size    => $search_hash->{check_limit},
1449             superpage_summary => $current_page_summary,
1450             facet_key         => $facet_key,
1451             ids               => \@results
1452         }
1453     );
1454
1455     cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1456
1457     return undef;
1458 }
1459
1460 sub tag_circulated_records {
1461     my ($auth, $results, $metabib) = @_;
1462     my $e = new_editor(authtoken => $auth);
1463     return $results unless $e->checkauth;
1464
1465     my $query = {
1466         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1467         from     => { acp => 'acn' }, 
1468         where    => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1469         distinct => 1
1470     };
1471
1472     if ($metabib) {
1473         $query = {
1474             select   => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1475             from     => 'mmsm',
1476             where    => { source => { in => $query } },
1477             distinct => 1
1478         };
1479     }
1480
1481     # Give me the distinct set of bib records that exist in the user's visible circulation history
1482     my $circ_recs = $e->json_query( $query );
1483
1484     # if the record appears in the circ history, push a 1 onto 
1485     # the rec array structure to indicate truthiness
1486     for my $rec (@$results) {
1487         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1488     }
1489
1490     $results
1491 }
1492
1493 # creates a unique token to represent the query in the cache
1494 sub search_cache_key {
1495     my $method = shift;
1496     my $search_hash = shift;
1497     my @sorted;
1498     for my $key (sort keys %$search_hash) {
1499         push(@sorted, ($key => $$search_hash{$key})) 
1500             unless $key eq 'limit'  or 
1501                    $key eq 'offset' or 
1502                    $key eq 'skip_check';
1503     }
1504     my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1505     return $pfx . md5_hex($method . $s);
1506 }
1507
1508 sub retrieve_cached_facets {
1509     my $self   = shift;
1510     my $client = shift;
1511     my $key    = shift;
1512     my $limit    = shift;
1513
1514     return undef unless ($key and $key =~ /_facets$/);
1515
1516     my $blob = $cache->get_cache($key) || {};
1517
1518     my $facets = {};
1519     if ($limit) {
1520        for my $f ( keys %$blob ) {
1521             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1522             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1523             for my $s ( @sorted ) {
1524                 my ($k) = keys(%$s);
1525                 my ($v) = values(%$s);
1526                 $$facets{$f}{$k} = $v;
1527             }
1528         }
1529     } else {
1530         $facets = $blob;
1531     }
1532
1533     return $facets;
1534 }
1535
1536 __PACKAGE__->register_method(
1537     method   => "retrieve_cached_facets",
1538     api_name => "open-ils.search.facet_cache.retrieve",
1539     signature => {
1540         desc   => 'Returns facet data derived from a specific search based on a key '.
1541                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1542         params => [
1543             {
1544                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1545                 type => 'string',
1546             }
1547         ],
1548         return => {
1549             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1550                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1551                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1552                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1553             type => 'object',
1554         }
1555     }
1556 );
1557
1558
1559 sub cache_facets {
1560     # add facets for this search to the facet cache
1561     my($key, $results, $metabib, $ignore) = @_;
1562     my $data = $cache->get_cache($key);
1563     $data ||= {};
1564
1565     return undef unless (@$results);
1566
1567     # The query we're constructing
1568     #
1569     # select  mfae.field as id,
1570     #         mfae.value,
1571     #         count(distinct mmrsm.appropriate-id-field )
1572     #   from  metabib.facet_entry mfae
1573     #         join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1574     #   where mmrsm.appropriate-id-field in IDLIST
1575     #   group by 1,2;
1576
1577     my $count_field = $metabib ? 'metarecord' : 'source';
1578     my $query = {   
1579         select  => {
1580             mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1581             mmrsm => [{
1582                 transform => 'count',
1583                 distinct => 1,
1584                 column => $count_field,
1585                 alias => 'count',
1586                 aggregate => 1
1587             }]
1588         },
1589         from    => {
1590             mfae => {
1591                 mmrsm => { field => 'source', fkey => 'source' },
1592                 cmf   => { field => 'id', fkey => 'field' }
1593             }
1594         },
1595         where   => {
1596             '+mmrsm' => { $count_field => $results },
1597             '+cmf'   => { facet_field => 't' }
1598         }
1599     };
1600
1601     $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1602         if ref($ignore) and @$ignore > 0;
1603
1604     my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1605
1606     for my $facet (@$facets) {
1607         next unless ($facet->{value});
1608         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1609     }
1610
1611     $logger->info("facet compilation: cached with key=$key");
1612
1613     $cache->put_cache($key, $data, $cache_timeout);
1614 }
1615
1616 sub cache_staged_search_page {
1617     # puts this set of results into the cache
1618     my($key, $page, $summary, $results) = @_;
1619     my $data = $cache->get_cache($key);
1620     $data ||= {};
1621     $data->{$page} = {
1622         summary => $summary,
1623         results => $results
1624     };
1625
1626     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1627         ($summary->{estimated_hit_count} || "none") .
1628         ", visible=" . ($summary->{visible} || "none")
1629     );
1630
1631     $cache->put_cache($key, $data, $cache_timeout);
1632 }
1633
1634 sub search_cache {
1635
1636     my $key     = shift;
1637     my $offset  = shift;
1638     my $limit   = shift;
1639     my $start   = $offset;
1640     my $end     = $offset + $limit - 1;
1641
1642     $logger->debug("searching cache for $key : $start..$end\n");
1643
1644     return undef unless $cache;
1645     my $data = $cache->get_cache($key);
1646
1647     return undef unless $data;
1648
1649     my $count = $data->[0];
1650     $data = $data->[1];
1651
1652     return undef unless $offset < $count;
1653
1654     my @result;
1655     for( my $i = $offset; $i <= $end; $i++ ) {
1656         last unless my $d = $$data[$i];
1657         push( @result, $d );
1658     }
1659
1660     $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1661
1662     return \@result;
1663 }
1664
1665
1666 sub put_cache {
1667     my( $key, $count, $data ) = @_;
1668     return undef unless $cache;
1669     $logger->debug("search_cache putting ".
1670         scalar(@$data)." items at key $key with timeout $cache_timeout");
1671     $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1672 }
1673
1674
1675 __PACKAGE__->register_method(
1676     method   => "biblio_mrid_to_modsbatch_batch",
1677     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1678 );
1679
1680 sub biblio_mrid_to_modsbatch_batch {
1681     my( $self, $client, $mrids) = @_;
1682     # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1683     my @mods;
1684     my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1685     for my $id (@$mrids) {
1686         next unless defined $id;
1687         my ($m) = $method->run($id);
1688         push @mods, $m;
1689     }
1690     return \@mods;
1691 }
1692
1693
1694 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1695              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1696     {
1697     __PACKAGE__->register_method(
1698         method    => "biblio_mrid_to_modsbatch",
1699         api_name  => $_,
1700         signature => {
1701             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1702                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1703             params => [
1704                 { desc => 'Metarecord ID', type => 'number' },
1705                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1706             ],
1707             return => {
1708                 desc => 'MVR Object, event on error',
1709             }
1710         }
1711     );
1712 }
1713
1714 sub biblio_mrid_to_modsbatch {
1715     my( $self, $client, $mrid, $args) = @_;
1716
1717     # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1718
1719     my ($mr, $evt) = _grab_metarecord($mrid);
1720     return $evt unless $mr;
1721
1722     my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1723               biblio_mrid_make_modsbatch($self, $client, $mr);
1724
1725     return $mvr unless ref($args);  
1726
1727     # Here we find the lead record appropriate for the given filters 
1728     # and use that for the title and author of the metarecord
1729     my $format = $$args{format};
1730     my $org    = $$args{org};
1731     my $depth  = $$args{depth};
1732
1733     return $mvr unless $format or $org or $depth;
1734
1735     my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1736     $method = "$method.staff" if $self->api_name =~ /staff/o; 
1737
1738     my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1739
1740     if( my $mods = $U->record_to_mvr($rec) ) {
1741
1742         $mvr->title( $mods->title );
1743         $mvr->author($mods->author);
1744         $logger->debug("mods_slim updating title and ".
1745             "author in mvr with ".$mods->title." : ".$mods->author);
1746     }
1747
1748     return $mvr;
1749 }
1750
1751 # converts a metarecord to an mvr
1752 sub _mr_to_mvr {
1753     my $mr = shift;
1754     my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1755     return Fieldmapper::metabib::virtual_record->new($perl);
1756 }
1757
1758 # checks to see if a metarecord has mods, if so returns true;
1759
1760 __PACKAGE__->register_method(
1761     method   => "biblio_mrid_check_mvr",
1762     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1763     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1764               . "if the metarecord already has an mvr associated with it."
1765 );
1766
1767 sub biblio_mrid_check_mvr {
1768     my( $self, $client, $mrid ) = @_;
1769     my $mr; 
1770
1771     my $evt;
1772     if(ref($mrid)) { $mr = $mrid; } 
1773     else { ($mr, $evt) = _grab_metarecord($mrid); }
1774     return $evt if $evt;
1775
1776     # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1777
1778     return _mr_to_mvr($mr) if $mr->mods();
1779     return undef;
1780 }
1781
1782 sub _grab_metarecord {
1783     my $mrid = shift;
1784     my $e = new_editor();
1785     my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1786     return ($mr);
1787 }
1788
1789
1790 __PACKAGE__->register_method(
1791     method   => "biblio_mrid_make_modsbatch",
1792     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1793     notes    => "Takes either a metarecord ID or a metarecord object. "
1794               . "Forces the creations of an mvr for the given metarecord. "
1795               . "The created mvr is returned."
1796 );
1797
1798 sub biblio_mrid_make_modsbatch {
1799     my( $self, $client, $mrid ) = @_;
1800
1801     my $e = new_editor();
1802
1803     my $mr;
1804     if( ref($mrid) ) {
1805         $mr = $mrid;
1806         $mrid = $mr->id;
1807     } else {
1808         $mr = $e->retrieve_metabib_metarecord($mrid) 
1809             or return $e->event;
1810     }
1811
1812     my $masterid = $mr->master_record;
1813     $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1814
1815     my $ids = $U->storagereq(
1816         'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1817     return undef unless @$ids;
1818
1819     my $master = $e->retrieve_biblio_record_entry($masterid)
1820         or return $e->event;
1821
1822     # start the mods batch
1823     my $u = OpenILS::Utils::ModsParser->new();
1824     $u->start_mods_batch( $master->marc );
1825
1826     # grab all of the sub-records and shove them into the batch
1827     my @ids = grep { $_ ne $masterid } @$ids;
1828     #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1829
1830     my $subrecs = [];
1831     if(@$ids) {
1832         for my $i (@$ids) {
1833             my $r = $e->retrieve_biblio_record_entry($i);
1834             push( @$subrecs, $r ) if $r;
1835         }
1836     }
1837
1838     for(@$subrecs) {
1839         $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1840         $u->push_mods_batch( $_->marc ) if $_->marc;
1841     }
1842
1843
1844     # finish up and send to the client
1845     my $mods = $u->finish_mods_batch();
1846     $mods->doc_id($mrid);
1847     $client->respond_complete($mods);
1848
1849
1850     # now update the mods string in the db
1851     my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1852     $mr->mods($string);
1853
1854     $e = new_editor(xact => 1);
1855     $e->update_metabib_metarecord($mr) 
1856         or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1857     $e->finish;
1858
1859     return undef;
1860 }
1861
1862
1863 # converts a mr id into a list of record ids
1864
1865 foreach (qw/open-ils.search.biblio.metarecord_to_records
1866             open-ils.search.biblio.metarecord_to_records.staff/)
1867 {
1868     __PACKAGE__->register_method(
1869         method    => "biblio_mrid_to_record_ids",
1870         api_name  => $_,
1871         signature => {
1872             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1873                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1874             params => [
1875                 { desc => 'Metarecord ID', type => 'number' },
1876                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1877             ],
1878             return => {
1879                 desc => 'Results object like {count => $i, ids =>[...]}',
1880                 type => 'object'
1881             }
1882             
1883         }
1884     );
1885 }
1886
1887 sub biblio_mrid_to_record_ids {
1888     my( $self, $client, $mrid, $args ) = @_;
1889
1890     my $format = $$args{format};
1891     my $org    = $$args{org};
1892     my $depth  = $$args{depth};
1893
1894     my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1895     $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1896     my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1897
1898     return { count => scalar(@$recs), ids => $recs };
1899 }
1900
1901
1902 __PACKAGE__->register_method(
1903     method   => "biblio_record_to_marc_html",
1904     api_name => "open-ils.search.biblio.record.html"
1905 );
1906
1907 __PACKAGE__->register_method(
1908     method   => "biblio_record_to_marc_html",
1909     api_name => "open-ils.search.authority.to_html"
1910 );
1911
1912 # Persistent parsers and setting objects
1913 my $parser = XML::LibXML->new();
1914 my $xslt   = XML::LibXSLT->new();
1915 my $marc_sheet;
1916 my $slim_marc_sheet;
1917 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1918
1919 sub biblio_record_to_marc_html {
1920     my($self, $client, $recordid, $slim, $marcxml) = @_;
1921
1922     my $sheet;
1923     my $dir = $settings_client->config_value("dirs", "xsl");
1924
1925     if($slim) {
1926         unless($slim_marc_sheet) {
1927             my $xsl = $settings_client->config_value(
1928                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1929             if($xsl) {
1930                 $xsl = $parser->parse_file("$dir/$xsl");
1931                 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1932             }
1933         }
1934         $sheet = $slim_marc_sheet;
1935     }
1936
1937     unless($sheet) {
1938         unless($marc_sheet) {
1939             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1940             my $xsl = $settings_client->config_value(
1941                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1942             $xsl = $parser->parse_file("$dir/$xsl");
1943             $marc_sheet = $xslt->parse_stylesheet($xsl);
1944         }
1945         $sheet = $marc_sheet;
1946     }
1947
1948     my $record;
1949     unless($marcxml) {
1950         my $e = new_editor();
1951         if($self->api_name =~ /authority/) {
1952             $record = $e->retrieve_authority_record_entry($recordid)
1953                 or return $e->event;
1954         } else {
1955             $record = $e->retrieve_biblio_record_entry($recordid)
1956                 or return $e->event;
1957         }
1958         $marcxml = $record->marc;
1959     }
1960
1961     my $xmldoc = $parser->parse_string($marcxml);
1962     my $html = $sheet->transform($xmldoc);
1963     return $html->documentElement->toString();
1964 }
1965
1966 __PACKAGE__->register_method(
1967     method    => "format_biblio_record_entry",
1968     api_name  => "open-ils.search.biblio.record.print",
1969     signature => {
1970         desc   => 'Returns a printable version of the specified bib record',
1971         params => [
1972             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1973         ],
1974         return => {
1975             desc => q/An action_trigger.event object or error event./,
1976             type => 'object',
1977         }
1978     }
1979 );
1980 __PACKAGE__->register_method(
1981     method    => "format_biblio_record_entry",
1982     api_name  => "open-ils.search.biblio.record.email",
1983     signature => {
1984         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1985         params => [
1986             { desc => 'Authentication token',  type => 'string'},
1987             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1988         ],
1989         return => {
1990             desc => q/Undefined on success, otherwise an error event./,
1991             type => 'object',
1992         }
1993     }
1994 );
1995
1996 sub format_biblio_record_entry {
1997     my($self, $conn, $arg1, $arg2) = @_;
1998
1999     my $for_print = ($self->api_name =~ /print/);
2000     my $for_email = ($self->api_name =~ /email/);
2001
2002     my $e; my $auth; my $bib_id; my $context_org;
2003
2004     if ($for_print) {
2005         $bib_id = $arg1;
2006         $context_org = $arg2 || $U->get_org_tree->id;
2007         $e = new_editor(xact => 1);
2008     } elsif ($for_email) {
2009         $auth = $arg1;
2010         $bib_id = $arg2;
2011         $e = new_editor(authtoken => $auth, xact => 1);
2012         return $e->die_event unless $e->checkauth;
2013         $context_org = $e->requestor->home_ou;
2014     }
2015
2016     my $bib_ids;
2017     if (ref $bib_id ne 'ARRAY') {
2018         $bib_ids = [ $bib_id ];
2019     } else {
2020         $bib_ids = $bib_id;
2021     }
2022
2023     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2024     $bucket->btype('temp');
2025     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2026     if ($for_email) {
2027         $bucket->owner($e->requestor) 
2028     } else {
2029         $bucket->owner(1);
2030     }
2031     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2032
2033     for my $id (@$bib_ids) {
2034
2035         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2036
2037         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2038         $bucket_entry->target_biblio_record_entry($bib);
2039         $bucket_entry->bucket($bucket_obj->id);
2040         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2041     }
2042
2043     $e->commit;
2044
2045     if ($for_print) {
2046
2047         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
2048
2049     } elsif ($for_email) {
2050
2051         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
2052     }
2053
2054     return undef;
2055 }
2056
2057
2058 __PACKAGE__->register_method(
2059     method   => "retrieve_all_copy_statuses",
2060     api_name => "open-ils.search.config.copy_status.retrieve.all"
2061 );
2062
2063 sub retrieve_all_copy_statuses {
2064     my( $self, $client ) = @_;
2065     return new_editor()->retrieve_all_config_copy_status();
2066 }
2067
2068
2069 __PACKAGE__->register_method(
2070     method   => "copy_counts_per_org",
2071     api_name => "open-ils.search.biblio.copy_counts.retrieve"
2072 );
2073
2074 __PACKAGE__->register_method(
2075     method   => "copy_counts_per_org",
2076     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2077 );
2078
2079 sub copy_counts_per_org {
2080     my( $self, $client, $record_id ) = @_;
2081
2082     warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2083
2084     my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2085     if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2086
2087     my $counts = $apputils->simple_scalar_request(
2088         "open-ils.storage", $method, $record_id );
2089
2090     $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2091     return $counts;
2092 }
2093
2094
2095 __PACKAGE__->register_method(
2096     method   => "copy_count_summary",
2097     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2098     notes    => "returns an array of these: "
2099               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2100               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2101 );
2102         
2103
2104 sub copy_count_summary {
2105     my( $self, $client, $rid, $org, $depth ) = @_;
2106     $org   ||= 1;
2107     $depth ||= 0;
2108     my $data = $U->storagereq(
2109         'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2110
2111     return [ sort {
2112         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2113         cmp
2114         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2115     } @$data ];
2116 }
2117
2118 __PACKAGE__->register_method(
2119     method   => "copy_location_count_summary",
2120     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2121     notes    => "returns an array of these: "
2122               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2123               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2124 );
2125
2126 sub copy_location_count_summary {
2127     my( $self, $client, $rid, $org, $depth ) = @_;
2128     $org   ||= 1;
2129     $depth ||= 0;
2130     my $data = $U->storagereq(
2131         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2132
2133     return [ sort {
2134         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2135         cmp
2136         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2137
2138         || $a->[4] cmp $b->[4]
2139     } @$data ];
2140 }
2141
2142 __PACKAGE__->register_method(
2143     method   => "copy_count_location_summary",
2144     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2145     notes    => "returns an array of these: "
2146               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2147               . "where statusx is a copy status name.  The statuses are sorted by ID."
2148 );
2149
2150 sub copy_count_location_summary {
2151     my( $self, $client, $rid, $org, $depth ) = @_;
2152     $org   ||= 1;
2153     $depth ||= 0;
2154     my $data = $U->storagereq(
2155         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2156     return [ sort {
2157         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2158         cmp
2159         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2160     } @$data ];
2161 }
2162
2163
2164 foreach (qw/open-ils.search.biblio.marc
2165             open-ils.search.biblio.marc.staff/)
2166 {
2167 __PACKAGE__->register_method(
2168     method    => "marc_search",
2169     api_name  => $_,
2170     signature => {
2171         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2172                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2173         params => [
2174             {
2175                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2176                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2177                 type => 'object'
2178             },
2179             {desc => 'limit (optional)',  type => 'number'},
2180             {desc => 'offset (optional)', type => 'number'}
2181         ],
2182         return => {
2183             desc => 'Results object like: { "count": $i, "ids": [...] }',
2184             type => 'object'
2185         }
2186     }
2187 );
2188 }
2189
2190 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2191
2192 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2193
2194     searches: complex query object  (required)
2195     org_unit: The org ID to focus the search at
2196     depth   : The org depth     
2197     limit   : integer search limit      default: 10
2198     offset  : integer search offset     default:  0
2199     sort    : What field to sort the results on? [ author | title | pubdate ]
2200     sort_dir: In what direction do we sort? [ asc | desc ]
2201
2202 Additional keys to refine search criteria:
2203
2204     audience : Audience
2205     language : Language (code)
2206     lit_form : Literary form
2207     item_form: Item form
2208     item_type: Item type
2209     format   : The MARC format
2210
2211 Please note that the specific strings to be used in the "addtional keys" will be entirely
2212 dependent on your loaded data.  
2213
2214 All keys except "searches" are optional.
2215 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2216
2217 For example, an arg hash might look like:
2218
2219     $arghash = {
2220         searches => [
2221             {
2222                 term     => "harry",
2223                 restrict => [
2224                     {
2225                         tag => 245,
2226                         subfield => "a"
2227                     }
2228                     # ...
2229                 ]
2230             }
2231             # ...
2232         ],
2233         org_unit  => 1,
2234         limit     => 5,
2235         sort      => "author",
2236         item_type => "g"
2237     }
2238
2239 The arghash is eventually passed to the SRF call:
2240 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2241
2242 Presently, search uses the cache unconditionally.
2243
2244 =cut
2245
2246 # FIXME: that example above isn't actually tested.
2247 # TODO: docache option?
2248 sub marc_search {
2249     my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2250
2251     my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2252     $method .= ".staff" if $self->api_name =~ /staff/;
2253     $method .= ".atomic";
2254
2255     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
2256     $offset ||=  0;     # FIXME: what about $args->{offset} ?
2257
2258     # allow caller to pass in a call timeout since MARC searches
2259     # can take longer than the default 60-second timeout.  
2260     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2261     $timeout = 120 if !$timeout or $timeout > 300;
2262
2263     my @search;
2264     push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2265     my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2266
2267     my $recs = search_cache($ckey, $offset, $limit);
2268
2269     if(!$recs) {
2270
2271         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2272         my $req = $ses->request($method, %$args);
2273         my $resp = $req->recv($timeout);
2274
2275         if($resp and $recs = $resp->content) {
2276             put_cache($ckey, scalar(@$recs), $recs);
2277             $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2278         } else {
2279             $recs = [];
2280         }
2281
2282         $ses->kill_me;
2283     }
2284
2285     my $count = 0;
2286     $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2287     my @recs = map { $_->[0] } @$recs;
2288
2289     return { ids => \@recs, count => $count };
2290 }
2291
2292
2293 foreach my $isbn_method (qw/
2294     open-ils.search.biblio.isbn
2295     open-ils.search.biblio.isbn.staff
2296 /) {
2297 __PACKAGE__->register_method(
2298     method    => "biblio_search_isbn",
2299     api_name  => $isbn_method,
2300     signature => {
2301         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2302         params => [
2303             {desc => 'ISBN', type => 'string'}
2304         ],
2305         return => {
2306             desc => 'Results object like: { "count": $i, "ids": [...] }',
2307             type => 'object'
2308         }
2309     }
2310 );
2311 }
2312
2313 sub biblio_search_isbn { 
2314     my( $self, $client, $isbn ) = @_;
2315     $logger->debug("Searching ISBN $isbn");
2316     # the previous implementation of this method was essentially unlimited,
2317     # so we will set our limit very high and let multiclass.query provide any
2318     # actual limit
2319     # XXX: if making this unlimited is deemed important, we might consider
2320     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2321     # which is functionally deprecated at this point, or a custom call to
2322     # 'open-ils.storage.biblio.multiclass.search_fts'
2323
2324     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2325     if ($self->api_name =~ m/.staff$/) {
2326         $isbn_method .= '.staff';
2327     }
2328
2329     my $method = $self->method_lookup($isbn_method);
2330     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2331     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2332     return { ids => \@recs, count => $search_result->{'count'} };
2333 }
2334
2335 __PACKAGE__->register_method(
2336     method   => "biblio_search_isbn_batch",
2337     api_name => "open-ils.search.biblio.isbn_list",
2338 );
2339
2340 # XXX: see biblio_search_isbn() for note concerning 'limit'
2341 sub biblio_search_isbn_batch { 
2342     my( $self, $client, $isbn_list ) = @_;
2343     $logger->debug("Searching ISBNs @$isbn_list");
2344     my @recs = (); my %rec_set = ();
2345     my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2346     foreach my $isbn ( @$isbn_list ) {
2347         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2348         my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2349         foreach my $rec (@recs_subset) {
2350             if (! $rec_set{ $rec }) {
2351                 $rec_set{ $rec } = 1;
2352                 push @recs, $rec;
2353             }
2354         }
2355     }
2356     return { ids => \@recs, count => scalar(@recs) };
2357 }
2358
2359 foreach my $issn_method (qw/
2360     open-ils.search.biblio.issn
2361     open-ils.search.biblio.issn.staff
2362 /) {
2363 __PACKAGE__->register_method(
2364     method   => "biblio_search_issn",
2365     api_name => $issn_method,
2366     signature => {
2367         desc   => 'Retrieve biblio IDs for a given ISSN',
2368         params => [
2369             {desc => 'ISBN', type => 'string'}
2370         ],
2371         return => {
2372             desc => 'Results object like: { "count": $i, "ids": [...] }',
2373             type => 'object'
2374         }
2375     }
2376 );
2377 }
2378
2379 sub biblio_search_issn { 
2380     my( $self, $client, $issn ) = @_;
2381     $logger->debug("Searching ISSN $issn");
2382     # the previous implementation of this method was essentially unlimited,
2383     # so we will set our limit very high and let multiclass.query provide any
2384     # actual limit
2385     # XXX: if making this unlimited is deemed important, we might consider
2386     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2387     # which is functionally deprecated at this point, or a custom call to
2388     # 'open-ils.storage.biblio.multiclass.search_fts'
2389
2390     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2391     if ($self->api_name =~ m/.staff$/) {
2392         $issn_method .= '.staff';
2393     }
2394
2395     my $method = $self->method_lookup($issn_method);
2396     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2397     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2398     return { ids => \@recs, count => $search_result->{'count'} };
2399 }
2400
2401
2402 __PACKAGE__->register_method(
2403     method    => "fetch_mods_by_copy",
2404     api_name  => "open-ils.search.biblio.mods_from_copy",
2405     argc      => 1,
2406     signature => {
2407         desc    => 'Retrieve MODS record given an attached copy ID',
2408         params  => [
2409             { desc => 'Copy ID', type => 'number' }
2410         ],
2411         returns => {
2412             desc => 'MODS record, event on error or uncataloged item'
2413         }
2414     }
2415 );
2416
2417 sub fetch_mods_by_copy {
2418     my( $self, $client, $copyid ) = @_;
2419     my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2420     return $evt if $evt;
2421     return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2422     return $apputils->record_to_mvr($record);
2423 }
2424
2425
2426 # -------------------------------------------------------------------------------------
2427
2428 __PACKAGE__->register_method(
2429     method   => "cn_browse",
2430     api_name => "open-ils.search.callnumber.browse.target",
2431     notes    => "Starts a callnumber browse"
2432 );
2433
2434 __PACKAGE__->register_method(
2435     method   => "cn_browse",
2436     api_name => "open-ils.search.callnumber.browse.page_up",
2437     notes    => "Returns the previous page of callnumbers",
2438 );
2439
2440 __PACKAGE__->register_method(
2441     method   => "cn_browse",
2442     api_name => "open-ils.search.callnumber.browse.page_down",
2443     notes    => "Returns the next page of callnumbers",
2444 );
2445
2446
2447 # RETURNS array of arrays like so: label, owning_lib, record, id
2448 sub cn_browse {
2449     my( $self, $client, @params ) = @_;
2450     my $method;
2451
2452     $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2453         if( $self->api_name =~ /target/ );
2454     $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2455         if( $self->api_name =~ /page_up/ );
2456     $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2457         if( $self->api_name =~ /page_down/ );
2458
2459     return $apputils->simplereq( 'open-ils.storage', $method, @params );
2460 }
2461 # -------------------------------------------------------------------------------------
2462
2463 __PACKAGE__->register_method(
2464     method        => "fetch_cn",
2465     api_name      => "open-ils.search.callnumber.retrieve",
2466     authoritative => 1,
2467     notes         => "retrieves a callnumber based on ID",
2468 );
2469
2470 sub fetch_cn {
2471     my( $self, $client, $id ) = @_;
2472
2473     my $e = new_editor();
2474     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2475     return $evt if $evt;
2476     return $cn;
2477 }
2478
2479 __PACKAGE__->register_method(
2480     method        => "fetch_fleshed_cn",
2481     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2482     authoritative => 1,
2483     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2484 );
2485
2486 sub fetch_fleshed_cn {
2487     my( $self, $client, $id ) = @_;
2488
2489     my $e = new_editor();
2490     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2491     return $evt if $evt;
2492     return $cn;
2493 }
2494
2495
2496 __PACKAGE__->register_method(
2497     method    => "fetch_copy_by_cn",
2498     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2499     signature => q/
2500         Returns an array of copy ID's by callnumber ID
2501         @param cnid The callnumber ID
2502         @return An array of copy IDs
2503     /
2504 );
2505
2506 sub fetch_copy_by_cn {
2507     my( $self, $conn, $cnid ) = @_;
2508     return $U->cstorereq(
2509         'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2510         { call_number => $cnid, deleted => 'f' } );
2511 }
2512
2513 __PACKAGE__->register_method(
2514     method    => 'fetch_cn_by_info',
2515     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2516     signature => q/
2517         @param label The callnumber label
2518         @param record The record the cn is attached to
2519         @param org The owning library of the cn
2520         @return The callnumber object
2521     /
2522 );
2523
2524
2525 sub fetch_cn_by_info {
2526     my( $self, $conn, $label, $record, $org ) = @_;
2527     return $U->cstorereq(
2528         'open-ils.cstore.direct.asset.call_number.search',
2529         { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2530 }
2531
2532
2533
2534 __PACKAGE__->register_method(
2535     method   => 'bib_extras',
2536     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2537     ctype => 'lit_form'
2538 );
2539 __PACKAGE__->register_method(
2540     method   => 'bib_extras',
2541     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2542     ctype => 'item_form'
2543 );
2544 __PACKAGE__->register_method(
2545     method   => 'bib_extras',
2546     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2547     ctype => 'item_type',
2548 );
2549 __PACKAGE__->register_method(
2550     method   => 'bib_extras',
2551     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2552     ctype => 'bib_level'
2553 );
2554 __PACKAGE__->register_method(
2555     method   => 'bib_extras',
2556     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2557     ctype => 'audience'
2558 );
2559
2560 sub bib_extras {
2561     my $self = shift;
2562     $logger->warn("deprecation warning: " .$self->api_name);
2563
2564     my $e = new_editor();
2565
2566     my $ctype = $self->{ctype};
2567     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2568
2569     my @objs;
2570     for my $ccvm (@$ccvms) {
2571         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2572         $obj->value($ccvm->value);
2573         $obj->code($ccvm->code);
2574         $obj->description($ccvm->description) if $obj->can('description');
2575         push(@objs, $obj);
2576     }
2577
2578     return \@objs;
2579 }
2580
2581
2582
2583 __PACKAGE__->register_method(
2584     method    => 'fetch_slim_record',
2585     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2586     signature => {
2587         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2588         params => [
2589             { desc => 'Array of Record IDs', type => 'array' }
2590         ],
2591         return => { 
2592             desc => 'Array of biblio records, event on error'
2593         }
2594     }
2595 );
2596
2597 sub fetch_slim_record {
2598     my( $self, $conn, $ids ) = @_;
2599
2600     my $editor = new_editor();
2601     my @res;
2602     for( @$ids ) {
2603         return $editor->event unless
2604             my $r = $editor->retrieve_biblio_record_entry($_);
2605         $r->clear_marc;
2606         push(@res, $r);
2607     }
2608     return \@res;
2609 }
2610
2611 __PACKAGE__->register_method(
2612     method    => 'rec_hold_parts',
2613     api_name  => 'open-ils.search.biblio.record_hold_parts',
2614     signature => q/
2615        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2616     /
2617 );
2618
2619 sub rec_hold_parts {
2620     my( $self, $conn, $args ) = @_;
2621
2622     my $rec        = $$args{record};
2623     my $mrec       = $$args{metarecord};
2624     my $pickup_lib = $$args{pickup_lib};
2625     my $e = new_editor();
2626
2627     my $query = {
2628         select => {bmp => ['id', 'label']},
2629         from => 'bmp',
2630         where => {
2631             id => {
2632                 in => {
2633                     select => {'acpm' => ['part']},
2634                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2635                     where => {
2636                         '+acp' => {'deleted' => 'f'},
2637                         '+bre' => {id => $rec}
2638                     },
2639                     distinct => 1,
2640                 }
2641             }
2642         },
2643         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2644     };
2645
2646     if(defined $pickup_lib) {
2647         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2648         if($hard_boundary) {
2649             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2650             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2651         }
2652     }
2653
2654     return $e->json_query($query);
2655 }
2656
2657
2658
2659
2660 __PACKAGE__->register_method(
2661     method    => 'rec_to_mr_rec_descriptors',
2662     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2663     signature => q/
2664         specialized method...
2665         Given a biblio record id or a metarecord id, 
2666         this returns a list of metabib.record_descriptor
2667         objects that live within the same metarecord
2668         @param args Object of args including:
2669     /
2670 );
2671
2672 sub rec_to_mr_rec_descriptors {
2673     my( $self, $conn, $args ) = @_;
2674
2675     my $rec        = $$args{record};
2676     my $mrec       = $$args{metarecord};
2677     my $item_forms = $$args{item_forms};
2678     my $item_types = $$args{item_types};
2679     my $item_lang  = $$args{item_lang};
2680     my $pickup_lib = $$args{pickup_lib};
2681
2682     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2683
2684     my $e = new_editor();
2685     my $recs;
2686
2687     if( !$mrec ) {
2688         my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2689         return $e->event unless @$map;
2690         $mrec = $$map[0]->metarecord;
2691     }
2692
2693     $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2694     return $e->event unless @$recs;
2695
2696     my @recs = map { $_->source } @$recs;
2697     my $search = { record => \@recs };
2698     $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2699     $search->{item_type} = $item_types if $item_types and @$item_types;
2700     $search->{item_lang} = $item_lang  if $item_lang;
2701
2702     my $desc = $e->search_metabib_record_descriptor($search);
2703
2704     my $query = {
2705         distinct => 1,
2706         select   => { 'bre' => ['id'] },
2707         from     => {
2708             'bre' => {
2709                 'acn' => {
2710                     'join' => {
2711                         'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2712                       }
2713                   }
2714              }
2715         },
2716         where => {
2717             '+bre' => { id => \@recs },
2718             '+acp' => {
2719                 holdable => 't',
2720                 deleted  => 'f'
2721             },
2722             "+ccs" => { holdable => 't' },
2723             "+acpl" => { holdable => 't' }
2724         }
2725     };
2726
2727     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2728         my $orgs = $e->json_query(
2729             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2730         ) or return $e->die_event;
2731
2732         $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2733     }
2734
2735     my $good_records = $e->json_query($query) or return $e->die_event;
2736
2737     my @keep;
2738     for my $d (@$desc) {
2739         if ( grep { $d->record == $_->{id} } @$good_records ) {
2740             push @keep, $d;
2741         }
2742     }
2743
2744     $desc = \@keep;
2745
2746     return { metarecord => $mrec, descriptors => $desc };
2747 }
2748
2749
2750 __PACKAGE__->register_method(
2751     method   => 'fetch_age_protect',
2752     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2753 );
2754
2755 sub fetch_age_protect {
2756     return new_editor()->retrieve_all_config_rule_age_hold_protect();
2757 }
2758
2759
2760 __PACKAGE__->register_method(
2761     method   => 'copies_by_cn_label',
2762     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2763 );
2764
2765 __PACKAGE__->register_method(
2766     method   => 'copies_by_cn_label',
2767     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2768 );
2769
2770 sub copies_by_cn_label {
2771     my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2772     my $e = new_editor();
2773     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2774     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2775     my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2776     return [] unless @$cns;
2777
2778     # show all non-deleted copies in the staff client ...
2779     if ($self->api_name =~ /staff$/o) {
2780         return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2781     }
2782
2783     # ... otherwise, grab the copies ...
2784     my $copies = $e->search_asset_copy(
2785         [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2786           {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2787         ]
2788     );
2789
2790     # ... and test for location and status visibility
2791     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2792 }
2793
2794
2795 1;
2796