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