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