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