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