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