]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Make sure the "running" indicator goes away
[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 = the_quest_for_knowledge( $self, $client, $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}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1388
1389                 if (defined($summary->{estimated_hit_count})) {
1390             foreach (qw/ checked visible excluded deleted /) {
1391                 $global_summary->{$_} += $summary->{$_};
1392             }
1393                         $global_summary->{total} = $summary->{total};
1394                 }
1395
1396         # we've found all the possible hits
1397         last if $current_count == $summary->{visible}
1398             and not defined $summary->{estimated_hit_count};
1399
1400         # we've found enough results to satisfy the requested limit/offset
1401         last if $current_count >= ($user_limit + $user_offset);
1402
1403         # we've scanned all possible hits
1404         if($summary->{checked} < $superpage_size) {
1405             $est_hit_count = scalar(@$all_results);
1406             # we have all possible results in hand, so we know the final hit count
1407             $is_real_hit_count = 1;
1408             last;
1409         }
1410     }
1411
1412     # Let other backends grab our data now that we're done.
1413     $cache_data = $cache->get_cache($key);
1414     if ($$cache_data{running} and $$cache_data{running} == $$) {
1415         delete $$cache_data{running};
1416         $cache->put_cache($key, $cache_data, $cache_timeout);
1417     }
1418
1419     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1420
1421         # refine the estimate if we have more than one superpage
1422         if ($page > 0 and not $is_real_hit_count) {
1423                 if ($global_summary->{checked} >= $global_summary->{total}) {
1424                         $est_hit_count = $global_summary->{visible};
1425                 } else {
1426                         my $updated_hit_count = $U->storagereq(
1427                                 'open-ils.storage.fts_paging_estimate',
1428                                 $global_summary->{checked},
1429                                 $global_summary->{visible},
1430                                 $global_summary->{excluded},
1431                                 $global_summary->{deleted},
1432                                 $global_summary->{total}
1433                         );
1434                         $est_hit_count = $updated_hit_count->{$estimation_strategy};
1435                 }
1436         }
1437
1438     $conn->respond_complete(
1439         {
1440             count             => $est_hit_count,
1441             core_limit        => $search_hash->{core_limit},
1442             superpage_size    => $search_hash->{check_limit},
1443             superpage_summary => $current_page_summary,
1444             facet_key         => $facet_key,
1445             ids               => \@results
1446         }
1447     );
1448
1449     cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1450
1451     return undef;
1452 }
1453
1454 sub tag_circulated_records {
1455     my ($auth, $results, $metabib) = @_;
1456     my $e = new_editor(authtoken => $auth);
1457     return $results unless $e->checkauth;
1458
1459     my $query = {
1460         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1461         from     => { acp => 'acn' }, 
1462         where    => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1463         distinct => 1
1464     };
1465
1466     if ($metabib) {
1467         $query = {
1468             select   => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1469             from     => 'mmsm',
1470             where    => { source => { in => $query } },
1471             distinct => 1
1472         };
1473     }
1474
1475     # Give me the distinct set of bib records that exist in the user's visible circulation history
1476     my $circ_recs = $e->json_query( $query );
1477
1478     # if the record appears in the circ history, push a 1 onto 
1479     # the rec array structure to indicate truthiness
1480     for my $rec (@$results) {
1481         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1482     }
1483
1484     $results
1485 }
1486
1487 # creates a unique token to represent the query in the cache
1488 sub search_cache_key {
1489     my $method = shift;
1490     my $search_hash = shift;
1491         my @sorted;
1492     for my $key (sort keys %$search_hash) {
1493             push(@sorted, ($key => $$search_hash{$key})) 
1494             unless $key eq 'limit'  or 
1495                    $key eq 'offset' or 
1496                    $key eq 'skip_check';
1497     }
1498         my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1499         return $pfx . md5_hex($method . $s);
1500 }
1501
1502 sub retrieve_cached_facets {
1503     my $self   = shift;
1504     my $client = shift;
1505     my $key    = shift;
1506     my $limit    = shift;
1507
1508     return undef unless ($key and $key =~ /_facets$/);
1509
1510     my $blob = $cache->get_cache($key) || {};
1511
1512     my $facets = {};
1513     if ($limit) {
1514        for my $f ( keys %$blob ) {
1515             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1516             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1517             for my $s ( @sorted ) {
1518                 my ($k) = keys(%$s);
1519                 my ($v) = values(%$s);
1520                 $$facets{$f}{$k} = $v;
1521             }
1522         }
1523     } else {
1524         $facets = $blob;
1525     }
1526
1527     return $facets;
1528 }
1529
1530 __PACKAGE__->register_method(
1531     method   => "retrieve_cached_facets",
1532     api_name => "open-ils.search.facet_cache.retrieve",
1533     signature => {
1534         desc   => 'Returns facet data derived from a specific search based on a key '.
1535                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1536         params => [
1537             {
1538                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1539                 type => 'string',
1540             }
1541         ],
1542         return => {
1543             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1544                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1545                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1546                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1547             type => 'object',
1548         }
1549     }
1550 );
1551
1552
1553 sub cache_facets {
1554     # add facets for this search to the facet cache
1555     my($key, $results, $metabib, $ignore) = @_;
1556     my $data = $cache->get_cache($key);
1557     $data ||= {};
1558
1559     return undef unless (@$results);
1560
1561     # The query we're constructing
1562     #
1563     # select  mfae.field as id,
1564     #         mfae.value,
1565     #         count(distinct mmrsm.appropriate-id-field )
1566     #   from  metabib.facet_entry mfae
1567     #         join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1568     #   where mmrsm.appropriate-id-field in IDLIST
1569     #   group by 1,2;
1570
1571     my $count_field = $metabib ? 'metarecord' : 'source';
1572     my $query = {   
1573         select  => {
1574             mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1575             mmrsm => [{
1576                 transform => 'count',
1577                 distinct => 1,
1578                 column => $count_field,
1579                 alias => 'count',
1580                 aggregate => 1
1581             }]
1582         },
1583         from    => {
1584             mfae => {
1585                 mmrsm => { field => 'source', fkey => 'source' },
1586                 cmf   => { field => 'id', fkey => 'field' }
1587             }
1588         },
1589         where   => {
1590             '+mmrsm' => { $count_field => $results },
1591             '+cmf'   => { facet_field => 't' }
1592         }
1593     };
1594
1595     $query->{where}->{'+cmf'}->{field_class} = {'not in' => $ignore}
1596         if ref($ignore) and @$ignore > 0;
1597
1598     my $facets = $U->cstorereq("open-ils.cstore.json_query.atomic", $query);
1599
1600     for my $facet (@$facets) {
1601         next unless ($facet->{value});
1602         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1603     }
1604
1605     $logger->info("facet compilation: cached with key=$key");
1606
1607     $cache->put_cache($key, $data, $cache_timeout);
1608 }
1609
1610 sub cache_staged_search_page {
1611     # puts this set of results into the cache
1612     my($key, $page, $summary, $results) = @_;
1613     my $data = $cache->get_cache($key);
1614     $data ||= {};
1615     $data->{$page} = {
1616         summary => $summary,
1617         results => $results
1618     };
1619
1620     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1621         $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1622
1623     $cache->put_cache($key, $data, $cache_timeout);
1624 }
1625
1626 sub search_cache {
1627
1628         my $key         = shift;
1629         my $offset      = shift;
1630         my $limit       = shift;
1631         my $start       = $offset;
1632         my $end         = $offset + $limit - 1;
1633
1634         $logger->debug("searching cache for $key : $start..$end\n");
1635
1636         return undef unless $cache;
1637         my $data = $cache->get_cache($key);
1638
1639         return undef unless $data;
1640
1641         my $count = $data->[0];
1642         $data = $data->[1];
1643
1644         return undef unless $offset < $count;
1645
1646         my @result;
1647         for( my $i = $offset; $i <= $end; $i++ ) {
1648                 last unless my $d = $$data[$i];
1649                 push( @result, $d );
1650         }
1651
1652         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1653
1654         return \@result;
1655 }
1656
1657
1658 sub put_cache {
1659         my( $key, $count, $data ) = @_;
1660         return undef unless $cache;
1661         $logger->debug("search_cache putting ".
1662                 scalar(@$data)." items at key $key with timeout $cache_timeout");
1663         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1664 }
1665
1666
1667 __PACKAGE__->register_method(
1668     method   => "biblio_mrid_to_modsbatch_batch",
1669     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1670 );
1671
1672 sub biblio_mrid_to_modsbatch_batch {
1673         my( $self, $client, $mrids) = @_;
1674         # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1675         my @mods;
1676         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1677         for my $id (@$mrids) {
1678                 next unless defined $id;
1679                 my ($m) = $method->run($id);
1680                 push @mods, $m;
1681         }
1682         return \@mods;
1683 }
1684
1685
1686 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1687              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1688     {
1689     __PACKAGE__->register_method(
1690         method    => "biblio_mrid_to_modsbatch",
1691         api_name  => $_,
1692         signature => {
1693             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1694                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1695             params => [
1696                 { desc => 'Metarecord ID', type => 'number' },
1697                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1698             ],
1699             return => {
1700                 desc => 'MVR Object, event on error',
1701             }
1702         }
1703     );
1704 }
1705
1706 sub biblio_mrid_to_modsbatch {
1707         my( $self, $client, $mrid, $args) = @_;
1708
1709         # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1710
1711         my ($mr, $evt) = _grab_metarecord($mrid);
1712         return $evt unless $mr;
1713
1714         my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1715               biblio_mrid_make_modsbatch($self, $client, $mr);
1716
1717         return $mvr unless ref($args);  
1718
1719         # Here we find the lead record appropriate for the given filters 
1720         # and use that for the title and author of the metarecord
1721     my $format = $$args{format};
1722     my $org    = $$args{org};
1723     my $depth  = $$args{depth};
1724
1725         return $mvr unless $format or $org or $depth;
1726
1727         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1728         $method = "$method.staff" if $self->api_name =~ /staff/o; 
1729
1730         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1731
1732         if( my $mods = $U->record_to_mvr($rec) ) {
1733
1734         $mvr->title( $mods->title );
1735         $mvr->author($mods->author);
1736                 $logger->debug("mods_slim updating title and ".
1737                         "author in mvr with ".$mods->title." : ".$mods->author);
1738         }
1739
1740         return $mvr;
1741 }
1742
1743 # converts a metarecord to an mvr
1744 sub _mr_to_mvr {
1745         my $mr = shift;
1746         my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1747         return Fieldmapper::metabib::virtual_record->new($perl);
1748 }
1749
1750 # checks to see if a metarecord has mods, if so returns true;
1751
1752 __PACKAGE__->register_method(
1753     method   => "biblio_mrid_check_mvr",
1754     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1755     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1756               . "if the metarecord already has an mvr associated with it."
1757 );
1758
1759 sub biblio_mrid_check_mvr {
1760         my( $self, $client, $mrid ) = @_;
1761         my $mr; 
1762
1763         my $evt;
1764         if(ref($mrid)) { $mr = $mrid; } 
1765         else { ($mr, $evt) = _grab_metarecord($mrid); }
1766         return $evt if $evt;
1767
1768         # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1769
1770         return _mr_to_mvr($mr) if $mr->mods();
1771         return undef;
1772 }
1773
1774 sub _grab_metarecord {
1775         my $mrid = shift;
1776         #my $e = OpenILS::Utils::Editor->new;
1777         my $e = new_editor();
1778         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1779         return ($mr);
1780 }
1781
1782
1783 __PACKAGE__->register_method(
1784     method   => "biblio_mrid_make_modsbatch",
1785     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1786     notes    => "Takes either a metarecord ID or a metarecord object. "
1787               . "Forces the creations of an mvr for the given metarecord. "
1788               . "The created mvr is returned."
1789 );
1790
1791 sub biblio_mrid_make_modsbatch {
1792         my( $self, $client, $mrid ) = @_;
1793
1794         #my $e = OpenILS::Utils::Editor->new;
1795         my $e = new_editor();
1796
1797         my $mr;
1798         if( ref($mrid) ) {
1799                 $mr = $mrid;
1800                 $mrid = $mr->id;
1801         } else {
1802                 $mr = $e->retrieve_metabib_metarecord($mrid) 
1803                         or return $e->event;
1804         }
1805
1806         my $masterid = $mr->master_record;
1807         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1808
1809         my $ids = $U->storagereq(
1810                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1811         return undef unless @$ids;
1812
1813         my $master = $e->retrieve_biblio_record_entry($masterid)
1814                 or return $e->event;
1815
1816         # start the mods batch
1817         my $u = OpenILS::Utils::ModsParser->new();
1818         $u->start_mods_batch( $master->marc );
1819
1820         # grab all of the sub-records and shove them into the batch
1821         my @ids = grep { $_ ne $masterid } @$ids;
1822         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1823
1824         my $subrecs = [];
1825         if(@$ids) {
1826                 for my $i (@$ids) {
1827                         my $r = $e->retrieve_biblio_record_entry($i);
1828                         push( @$subrecs, $r ) if $r;
1829                 }
1830         }
1831
1832         for(@$subrecs) {
1833                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1834                 $u->push_mods_batch( $_->marc ) if $_->marc;
1835         }
1836
1837
1838         # finish up and send to the client
1839         my $mods = $u->finish_mods_batch();
1840         $mods->doc_id($mrid);
1841         $client->respond_complete($mods);
1842
1843
1844         # now update the mods string in the db
1845         my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1846         $mr->mods($string);
1847
1848         #$e = OpenILS::Utils::Editor->new(xact => 1);
1849         $e = new_editor(xact => 1);
1850         $e->update_metabib_metarecord($mr) 
1851                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1852         $e->finish;
1853
1854         return undef;
1855 }
1856
1857
1858 # converts a mr id into a list of record ids
1859
1860 foreach (qw/open-ils.search.biblio.metarecord_to_records
1861             open-ils.search.biblio.metarecord_to_records.staff/)
1862 {
1863     __PACKAGE__->register_method(
1864         method    => "biblio_mrid_to_record_ids",
1865         api_name  => $_,
1866         signature => {
1867             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1868                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1869             params => [
1870                 { desc => 'Metarecord ID', type => 'number' },
1871                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1872             ],
1873             return => {
1874                 desc => 'Results object like {count => $i, ids =>[...]}',
1875                 type => 'object'
1876             }
1877             
1878         }
1879     );
1880 }
1881
1882 sub biblio_mrid_to_record_ids {
1883         my( $self, $client, $mrid, $args ) = @_;
1884
1885     my $format = $$args{format};
1886     my $org    = $$args{org};
1887     my $depth  = $$args{depth};
1888
1889         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1890         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1891         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1892
1893         return { count => scalar(@$recs), ids => $recs };
1894 }
1895
1896
1897 __PACKAGE__->register_method(
1898     method   => "biblio_record_to_marc_html",
1899     api_name => "open-ils.search.biblio.record.html"
1900 );
1901
1902 __PACKAGE__->register_method(
1903     method   => "biblio_record_to_marc_html",
1904     api_name => "open-ils.search.authority.to_html"
1905 );
1906
1907 # Persistent parsers and setting objects
1908 my $parser = XML::LibXML->new();
1909 my $xslt   = XML::LibXSLT->new();
1910 my $marc_sheet;
1911 my $slim_marc_sheet;
1912 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1913
1914 sub biblio_record_to_marc_html {
1915         my($self, $client, $recordid, $slim, $marcxml) = @_;
1916
1917     my $sheet;
1918         my $dir = $settings_client->config_value("dirs", "xsl");
1919
1920     if($slim) {
1921         unless($slim_marc_sheet) {
1922                     my $xsl = $settings_client->config_value(
1923                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1924             if($xsl) {
1925                         $xsl = $parser->parse_file("$dir/$xsl");
1926                         $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1927             }
1928         }
1929         $sheet = $slim_marc_sheet;
1930     }
1931
1932     unless($sheet) {
1933         unless($marc_sheet) {
1934             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1935                     my $xsl = $settings_client->config_value(
1936                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1937                     $xsl = $parser->parse_file("$dir/$xsl");
1938                     $marc_sheet = $xslt->parse_stylesheet($xsl);
1939         }
1940         $sheet = $marc_sheet;
1941     }
1942
1943     my $record;
1944     unless($marcxml) {
1945         my $e = new_editor();
1946         if($self->api_name =~ /authority/) {
1947             $record = $e->retrieve_authority_record_entry($recordid)
1948                 or return $e->event;
1949         } else {
1950             $record = $e->retrieve_biblio_record_entry($recordid)
1951                 or return $e->event;
1952         }
1953         $marcxml = $record->marc;
1954     }
1955
1956         my $xmldoc = $parser->parse_string($marcxml);
1957         my $html = $sheet->transform($xmldoc);
1958         return $html->documentElement->toString();
1959 }
1960
1961 __PACKAGE__->register_method(
1962     method    => "format_biblio_record_entry",
1963     api_name  => "open-ils.search.biblio.record.print",
1964     signature => {
1965         desc   => 'Returns a printable version of the specified bib record',
1966         params => [
1967             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1968         ],
1969         return => {
1970             desc => q/An action_trigger.event object or error event./,
1971             type => 'object',
1972         }
1973     }
1974 );
1975 __PACKAGE__->register_method(
1976     method    => "format_biblio_record_entry",
1977     api_name  => "open-ils.search.biblio.record.email",
1978     signature => {
1979         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1980         params => [
1981             { desc => 'Authentication token',  type => 'string'},
1982             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1983         ],
1984         return => {
1985             desc => q/Undefined on success, otherwise an error event./,
1986             type => 'object',
1987         }
1988     }
1989 );
1990
1991 sub format_biblio_record_entry {
1992     my($self, $conn, $arg1, $arg2) = @_;
1993
1994     my $for_print = ($self->api_name =~ /print/);
1995     my $for_email = ($self->api_name =~ /email/);
1996
1997     my $e; my $auth; my $bib_id; my $context_org;
1998
1999     if ($for_print) {
2000         $bib_id = $arg1;
2001         $context_org = $arg2 || $U->get_org_tree->id;
2002         $e = new_editor(xact => 1);
2003     } elsif ($for_email) {
2004         $auth = $arg1;
2005         $bib_id = $arg2;
2006         $e = new_editor(authtoken => $auth, xact => 1);
2007         return $e->die_event unless $e->checkauth;
2008         $context_org = $e->requestor->home_ou;
2009     }
2010
2011     my $bib_ids;
2012     if (ref $bib_id ne 'ARRAY') {
2013         $bib_ids = [ $bib_id ];
2014     } else {
2015         $bib_ids = $bib_id;
2016     }
2017
2018     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2019     $bucket->btype('temp');
2020     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2021     if ($for_email) {
2022         $bucket->owner($e->requestor) 
2023     } else {
2024         $bucket->owner(1);
2025     }
2026     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2027
2028     for my $id (@$bib_ids) {
2029
2030         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2031
2032         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2033         $bucket_entry->target_biblio_record_entry($bib);
2034         $bucket_entry->bucket($bucket_obj->id);
2035         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2036     }
2037
2038     $e->commit;
2039
2040     if ($for_print) {
2041
2042         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
2043
2044     } elsif ($for_email) {
2045
2046         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
2047     }
2048
2049     return undef;
2050 }
2051
2052
2053 __PACKAGE__->register_method(
2054     method   => "retrieve_all_copy_statuses",
2055     api_name => "open-ils.search.config.copy_status.retrieve.all"
2056 );
2057
2058 sub retrieve_all_copy_statuses {
2059         my( $self, $client ) = @_;
2060         return new_editor()->retrieve_all_config_copy_status();
2061 }
2062
2063
2064 __PACKAGE__->register_method(
2065     method   => "copy_counts_per_org",
2066     api_name => "open-ils.search.biblio.copy_counts.retrieve"
2067 );
2068
2069 __PACKAGE__->register_method(
2070     method   => "copy_counts_per_org",
2071     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2072 );
2073
2074 sub copy_counts_per_org {
2075         my( $self, $client, $record_id ) = @_;
2076
2077         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2078
2079         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2080         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2081
2082         my $counts = $apputils->simple_scalar_request(
2083                 "open-ils.storage", $method, $record_id );
2084
2085         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2086         return $counts;
2087 }
2088
2089
2090 __PACKAGE__->register_method(
2091     method   => "copy_count_summary",
2092     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2093     notes    => "returns an array of these: "
2094               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2095               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2096 );
2097                 
2098
2099 sub copy_count_summary {
2100         my( $self, $client, $rid, $org, $depth ) = @_;
2101     $org   ||= 1;
2102     $depth ||= 0;
2103     my $data = $U->storagereq(
2104                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2105
2106     return [ sort {
2107         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2108         cmp
2109         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2110     } @$data ];
2111 }
2112
2113 __PACKAGE__->register_method(
2114     method   => "copy_location_count_summary",
2115     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2116     notes    => "returns an array of these: "
2117               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2118               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2119 );
2120
2121 sub copy_location_count_summary {
2122     my( $self, $client, $rid, $org, $depth ) = @_;
2123     $org   ||= 1;
2124     $depth ||= 0;
2125     my $data = $U->storagereq(
2126                 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2127
2128     return [ sort {
2129         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2130         cmp
2131         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2132
2133         || $a->[4] cmp $b->[4]
2134     } @$data ];
2135 }
2136
2137 __PACKAGE__->register_method(
2138     method   => "copy_count_location_summary",
2139     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2140     notes    => "returns an array of these: "
2141               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2142               . "where statusx is a copy status name.  The statuses are sorted by ID."
2143 );
2144
2145 sub copy_count_location_summary {
2146     my( $self, $client, $rid, $org, $depth ) = @_;
2147     $org   ||= 1;
2148     $depth ||= 0;
2149     my $data = $U->storagereq(
2150         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2151     return [ sort {
2152         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2153         cmp
2154         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2155     } @$data ];
2156 }
2157
2158
2159 foreach (qw/open-ils.search.biblio.marc
2160             open-ils.search.biblio.marc.staff/)
2161 {
2162 __PACKAGE__->register_method(
2163     method    => "marc_search",
2164     api_name  => $_,
2165     signature => {
2166         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2167                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2168         params => [
2169             {
2170                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2171                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2172                 type => 'object'
2173             },
2174             {desc => 'limit (optional)',  type => 'number'},
2175             {desc => 'offset (optional)', type => 'number'}
2176         ],
2177         return => {
2178             desc => 'Results object like: { "count": $i, "ids": [...] }',
2179             type => 'object'
2180         }
2181     }
2182 );
2183 }
2184
2185 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
2186
2187 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2188
2189     searches: complex query object  (required)
2190     org_unit: The org ID to focus the search at
2191     depth   : The org depth     
2192     limit   : integer search limit      default: 10
2193     offset  : integer search offset     default:  0
2194     sort    : What field to sort the results on? [ author | title | pubdate ]
2195     sort_dir: In what direction do we sort? [ asc | desc ]
2196
2197 Additional keys to refine search criteria:
2198
2199     audience : Audience
2200     language : Language (code)
2201     lit_form : Literary form
2202     item_form: Item form
2203     item_type: Item type
2204     format   : The MARC format
2205
2206 Please note that the specific strings to be used in the "addtional keys" will be entirely
2207 dependent on your loaded data.  
2208
2209 All keys except "searches" are optional.
2210 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2211
2212 For example, an arg hash might look like:
2213
2214     $arghash = {
2215         searches => [
2216             {
2217                 term     => "harry",
2218                 restrict => [
2219                     {
2220                         tag => 245,
2221                         subfield => "a"
2222                     }
2223                     # ...
2224                 ]
2225             }
2226             # ...
2227         ],
2228         org_unit  => 1,
2229         limit     => 5,
2230         sort      => "author",
2231         item_type => "g"
2232     }
2233
2234 The arghash is eventually passed to the SRF call:
2235 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2236
2237 Presently, search uses the cache unconditionally.
2238
2239 =cut
2240
2241 # FIXME: that example above isn't actually tested.
2242 # TODO: docache option?
2243 sub marc_search {
2244         my( $self, $conn, $args, $limit, $offset, $timeout ) = @_;
2245
2246         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2247         $method .= ".staff" if $self->api_name =~ /staff/;
2248         $method .= ".atomic";
2249
2250     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
2251     $offset ||=  0;     # FIXME: what about $args->{offset} ?
2252
2253     # allow caller to pass in a call timeout since MARC searches
2254     # can take longer than the default 60-second timeout.  
2255     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2256     $timeout = 120 if !$timeout or $timeout > 300;
2257
2258         my @search;
2259         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2260         my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2261
2262         my $recs = search_cache($ckey, $offset, $limit);
2263
2264         if(!$recs) {
2265
2266         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2267         my $req = $ses->request($method, %$args);
2268         my $resp = $req->recv($timeout);
2269
2270         if($resp and $recs = $resp->content) {
2271                         put_cache($ckey, scalar(@$recs), $recs);
2272                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2273                 } else {
2274                         $recs = [];
2275                 }
2276
2277         $ses->kill_me;
2278         }
2279
2280         my $count = 0;
2281         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2282         my @recs = map { $_->[0] } @$recs;
2283
2284         return { ids => \@recs, count => $count };
2285 }
2286
2287
2288 foreach my $isbn_method (qw/
2289     open-ils.search.biblio.isbn
2290     open-ils.search.biblio.isbn.staff
2291 /) {
2292 __PACKAGE__->register_method(
2293     method    => "biblio_search_isbn",
2294     api_name  => $isbn_method,
2295     signature => {
2296         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2297         params => [
2298             {desc => 'ISBN', type => 'string'}
2299         ],
2300         return => {
2301             desc => 'Results object like: { "count": $i, "ids": [...] }',
2302             type => 'object'
2303         }
2304     }
2305 );
2306 }
2307
2308 sub biblio_search_isbn { 
2309         my( $self, $client, $isbn ) = @_;
2310         $logger->debug("Searching ISBN $isbn");
2311         # the previous implementation of this method was essentially unlimited,
2312         # so we will set our limit very high and let multiclass.query provide any
2313         # actual limit
2314         # XXX: if making this unlimited is deemed important, we might consider
2315         # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2316         # which is functionally deprecated at this point, or a custom call to
2317         # 'open-ils.storage.biblio.multiclass.search_fts'
2318
2319     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2320     if ($self->api_name =~ m/.staff$/) {
2321         $isbn_method .= '.staff';
2322     }
2323
2324         my $method = $self->method_lookup($isbn_method);
2325         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2326         my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2327         return { ids => \@recs, count => $search_result->{'count'} };
2328 }
2329
2330 __PACKAGE__->register_method(
2331     method   => "biblio_search_isbn_batch",
2332     api_name => "open-ils.search.biblio.isbn_list",
2333 );
2334
2335 # XXX: see biblio_search_isbn() for note concerning 'limit'
2336 sub biblio_search_isbn_batch { 
2337         my( $self, $client, $isbn_list ) = @_;
2338         $logger->debug("Searching ISBNs @$isbn_list");
2339         my @recs = (); my %rec_set = ();
2340         my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2341         foreach my $isbn ( @$isbn_list ) {
2342                 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2343                 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2344                 foreach my $rec (@recs_subset) {
2345                         if (! $rec_set{ $rec }) {
2346                                 $rec_set{ $rec } = 1;
2347                                 push @recs, $rec;
2348                         }
2349                 }
2350         }
2351         return { ids => \@recs, count => scalar(@recs) };
2352 }
2353
2354 foreach my $issn_method (qw/
2355     open-ils.search.biblio.issn
2356     open-ils.search.biblio.issn.staff
2357 /) {
2358 __PACKAGE__->register_method(
2359     method   => "biblio_search_issn",
2360     api_name => $issn_method,
2361     signature => {
2362         desc   => 'Retrieve biblio IDs for a given ISSN',
2363         params => [
2364             {desc => 'ISBN', type => 'string'}
2365         ],
2366         return => {
2367             desc => 'Results object like: { "count": $i, "ids": [...] }',
2368             type => 'object'
2369         }
2370     }
2371 );
2372 }
2373
2374 sub biblio_search_issn { 
2375         my( $self, $client, $issn ) = @_;
2376         $logger->debug("Searching ISSN $issn");
2377         # the previous implementation of this method was essentially unlimited,
2378         # so we will set our limit very high and let multiclass.query provide any
2379         # actual limit
2380         # XXX: if making this unlimited is deemed important, we might consider
2381         # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2382         # which is functionally deprecated at this point, or a custom call to
2383         # 'open-ils.storage.biblio.multiclass.search_fts'
2384
2385     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2386     if ($self->api_name =~ m/.staff$/) {
2387         $issn_method .= '.staff';
2388     }
2389
2390         my $method = $self->method_lookup($issn_method);
2391         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2392         my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2393         return { ids => \@recs, count => $search_result->{'count'} };
2394 }
2395
2396
2397 __PACKAGE__->register_method(
2398     method    => "fetch_mods_by_copy",
2399     api_name  => "open-ils.search.biblio.mods_from_copy",
2400     argc      => 1,
2401     signature => {
2402         desc    => 'Retrieve MODS record given an attached copy ID',
2403         params  => [
2404             { desc => 'Copy ID', type => 'number' }
2405         ],
2406         returns => {
2407             desc => 'MODS record, event on error or uncataloged item'
2408         }
2409     }
2410 );
2411
2412 sub fetch_mods_by_copy {
2413         my( $self, $client, $copyid ) = @_;
2414         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2415         return $evt if $evt;
2416         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2417         return $apputils->record_to_mvr($record);
2418 }
2419
2420
2421 # -------------------------------------------------------------------------------------
2422
2423 __PACKAGE__->register_method(
2424     method   => "cn_browse",
2425     api_name => "open-ils.search.callnumber.browse.target",
2426     notes    => "Starts a callnumber browse"
2427 );
2428
2429 __PACKAGE__->register_method(
2430     method   => "cn_browse",
2431     api_name => "open-ils.search.callnumber.browse.page_up",
2432     notes    => "Returns the previous page of callnumbers",
2433 );
2434
2435 __PACKAGE__->register_method(
2436     method   => "cn_browse",
2437     api_name => "open-ils.search.callnumber.browse.page_down",
2438     notes    => "Returns the next page of callnumbers",
2439 );
2440
2441
2442 # RETURNS array of arrays like so: label, owning_lib, record, id
2443 sub cn_browse {
2444         my( $self, $client, @params ) = @_;
2445         my $method;
2446
2447         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2448                 if( $self->api_name =~ /target/ );
2449         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2450                 if( $self->api_name =~ /page_up/ );
2451         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2452                 if( $self->api_name =~ /page_down/ );
2453
2454         return $apputils->simplereq( 'open-ils.storage', $method, @params );
2455 }
2456 # -------------------------------------------------------------------------------------
2457
2458 __PACKAGE__->register_method(
2459     method        => "fetch_cn",
2460     api_name      => "open-ils.search.callnumber.retrieve",
2461     authoritative => 1,
2462     notes         => "retrieves a callnumber based on ID",
2463 );
2464
2465 sub fetch_cn {
2466         my( $self, $client, $id ) = @_;
2467
2468         my $e = new_editor();
2469         my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2470         return $evt if $evt;
2471         return $cn;
2472 }
2473
2474 __PACKAGE__->register_method(
2475     method        => "fetch_fleshed_cn",
2476     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2477     authoritative => 1,
2478     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2479 );
2480
2481 sub fetch_fleshed_cn {
2482         my( $self, $client, $id ) = @_;
2483
2484         my $e = new_editor();
2485         my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2486         return $evt if $evt;
2487         return $cn;
2488 }
2489
2490
2491 __PACKAGE__->register_method(
2492     method    => "fetch_copy_by_cn",
2493     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2494     signature => q/
2495                 Returns an array of copy ID's by callnumber ID
2496                 @param cnid The callnumber ID
2497                 @return An array of copy IDs
2498         /
2499 );
2500
2501 sub fetch_copy_by_cn {
2502         my( $self, $conn, $cnid ) = @_;
2503         return $U->cstorereq(
2504                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2505                 { call_number => $cnid, deleted => 'f' } );
2506 }
2507
2508 __PACKAGE__->register_method(
2509     method    => 'fetch_cn_by_info',
2510     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2511     signature => q/
2512                 @param label The callnumber label
2513                 @param record The record the cn is attached to
2514                 @param org The owning library of the cn
2515                 @return The callnumber object
2516         /
2517 );
2518
2519
2520 sub fetch_cn_by_info {
2521         my( $self, $conn, $label, $record, $org ) = @_;
2522         return $U->cstorereq(
2523                 'open-ils.cstore.direct.asset.call_number.search',
2524                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2525 }
2526
2527
2528
2529 __PACKAGE__->register_method(
2530     method   => 'bib_extras',
2531     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2532     ctype => 'lit_form'
2533 );
2534 __PACKAGE__->register_method(
2535     method   => 'bib_extras',
2536     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2537     ctype => 'item_form'
2538 );
2539 __PACKAGE__->register_method(
2540     method   => 'bib_extras',
2541     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2542     ctype => 'item_type',
2543 );
2544 __PACKAGE__->register_method(
2545     method   => 'bib_extras',
2546     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2547     ctype => 'bib_level'
2548 );
2549 __PACKAGE__->register_method(
2550     method   => 'bib_extras',
2551     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2552     ctype => 'audience'
2553 );
2554
2555 sub bib_extras {
2556         my $self = shift;
2557     $logger->warn("deprecation warning: " .$self->api_name);
2558
2559         my $e = new_editor();
2560
2561     my $ctype = $self->{ctype};
2562     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2563
2564     my @objs;
2565     for my $ccvm (@$ccvms) {
2566         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2567         $obj->value($ccvm->value);
2568         $obj->code($ccvm->code);
2569         $obj->description($ccvm->description) if $obj->can('description');
2570         push(@objs, $obj);
2571     }
2572
2573     return \@objs;
2574 }
2575
2576
2577
2578 __PACKAGE__->register_method(
2579     method    => 'fetch_slim_record',
2580     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2581     signature => {
2582         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2583         params => [
2584             { desc => 'Array of Record IDs', type => 'array' }
2585         ],
2586         return => { 
2587             desc => 'Array of biblio records, event on error'
2588         }
2589     }
2590 );
2591
2592 sub fetch_slim_record {
2593     my( $self, $conn, $ids ) = @_;
2594
2595 #my $editor = OpenILS::Utils::Editor->new;
2596     my $editor = new_editor();
2597         my @res;
2598     for( @$ids ) {
2599         return $editor->event unless
2600             my $r = $editor->retrieve_biblio_record_entry($_);
2601         $r->clear_marc;
2602         push(@res, $r);
2603     }
2604     return \@res;
2605 }
2606
2607 __PACKAGE__->register_method(
2608     method    => 'rec_hold_parts',
2609     api_name  => 'open-ils.search.biblio.record_hold_parts',
2610     signature => q/
2611        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2612         /
2613 );
2614
2615 sub rec_hold_parts {
2616         my( $self, $conn, $args ) = @_;
2617
2618     my $rec        = $$args{record};
2619     my $mrec       = $$args{metarecord};
2620     my $pickup_lib = $$args{pickup_lib};
2621     my $e = new_editor();
2622
2623     my $query = {
2624         select => {bmp => ['id', 'label']},
2625         from => 'bmp',
2626         where => {
2627             id => {
2628                 in => {
2629                     select => {'acpm' => ['part']},
2630                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2631                     where => {
2632                         '+acp' => {'deleted' => 'f'},
2633                         '+bre' => {id => $rec}
2634                     },
2635                     distinct => 1,
2636                 }
2637             }
2638         },
2639         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2640     };
2641
2642     if(defined $pickup_lib) {
2643         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2644         if($hard_boundary) {
2645             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2646             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2647         }
2648     }
2649
2650     return $e->json_query($query);
2651 }
2652
2653
2654
2655
2656 __PACKAGE__->register_method(
2657     method    => 'rec_to_mr_rec_descriptors',
2658     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2659     signature => q/
2660                 specialized method...
2661                 Given a biblio record id or a metarecord id, 
2662                 this returns a list of metabib.record_descriptor
2663                 objects that live within the same metarecord
2664                 @param args Object of args including:
2665         /
2666 );
2667
2668 sub rec_to_mr_rec_descriptors {
2669         my( $self, $conn, $args ) = @_;
2670
2671     my $rec        = $$args{record};
2672     my $mrec       = $$args{metarecord};
2673     my $item_forms = $$args{item_forms};
2674     my $item_types = $$args{item_types};
2675     my $item_lang  = $$args{item_lang};
2676     my $pickup_lib = $$args{pickup_lib};
2677
2678     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2679
2680         my $e = new_editor();
2681         my $recs;
2682
2683         if( !$mrec ) {
2684                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2685                 return $e->event unless @$map;
2686                 $mrec = $$map[0]->metarecord;
2687         }
2688
2689         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2690         return $e->event unless @$recs;
2691
2692         my @recs = map { $_->source } @$recs;
2693         my $search = { record => \@recs };
2694         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2695         $search->{item_type} = $item_types if $item_types and @$item_types;
2696         $search->{item_lang} = $item_lang  if $item_lang;
2697
2698         my $desc = $e->search_metabib_record_descriptor($search);
2699
2700         my $query = {
2701                 distinct => 1,
2702                 select   => { 'bre' => ['id'] },
2703                 from     => {
2704                         'bre' => {
2705                                 'acn' => {
2706                                         'join' => {
2707                                                 'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2708                                           }
2709                                   }
2710                          }
2711                 },
2712                 where => {
2713                         '+bre' => { id => \@recs },
2714                         '+acp' => {
2715                                 holdable => 't',
2716                                 deleted  => 'f'
2717                         },
2718                         "+ccs" => { holdable => 't' },
2719                         "+acpl" => { holdable => 't' }
2720                 }
2721         };
2722
2723         if ($hard_boundary) { # 0 (or "top") is the same as no setting
2724                 my $orgs = $e->json_query(
2725                         { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2726                 ) or return $e->die_event;
2727
2728                 $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2729         }
2730
2731         my $good_records = $e->json_query($query) or return $e->die_event;
2732
2733         my @keep;
2734         for my $d (@$desc) {
2735                 if ( grep { $d->record == $_->{id} } @$good_records ) {
2736                         push @keep, $d;
2737                 }
2738         }
2739
2740         $desc = \@keep;
2741
2742         return { metarecord => $mrec, descriptors => $desc };
2743 }
2744
2745
2746 __PACKAGE__->register_method(
2747     method   => 'fetch_age_protect',
2748     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2749 );
2750
2751 sub fetch_age_protect {
2752         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2753 }
2754
2755
2756 __PACKAGE__->register_method(
2757     method   => 'copies_by_cn_label',
2758     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2759 );
2760
2761 __PACKAGE__->register_method(
2762     method   => 'copies_by_cn_label',
2763     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2764 );
2765
2766 sub copies_by_cn_label {
2767         my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2768         my $e = new_editor();
2769     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2770     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2771         my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2772         return [] unless @$cns;
2773
2774         # show all non-deleted copies in the staff client ...
2775         if ($self->api_name =~ /staff$/o) {
2776                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2777         }
2778
2779         # ... otherwise, grab the copies ...
2780         my $copies = $e->search_asset_copy(
2781                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2782                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2783                 ]
2784         );
2785
2786         # ... and test for location and status visibility
2787         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2788 }
2789
2790
2791 1;
2792