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