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