]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
LP#1749475 Improve error handling in send_event_email_output
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Search / Biblio.pm
1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
4
5
6 use OpenSRF::Utils::JSON;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::ModsParser;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenSRF::Utils::Cache;
12 use Encode;
13 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("send_event_email_output: Email failed with error: $err");
1884     };
1885
1886     if( !$err and $stat and $stat->type eq 'success' ) {
1887         $logger->info("send_event_email_output: successfully sent email");
1888         return 1;
1889     } else {
1890         $logger->warn("send_event_email_output: unable to send email: ".Dumper($stat));
1891         return 0;
1892     }
1893 }
1894
1895 __PACKAGE__->register_method(
1896     method    => "format_biblio_record_entry",
1897     api_name  => "open-ils.search.biblio.record.print.preview",
1898 );
1899
1900 __PACKAGE__->register_method(
1901     method    => "format_biblio_record_entry",
1902     api_name  => "open-ils.search.biblio.record.email.preview",
1903 );
1904
1905 __PACKAGE__->register_method(
1906     method    => "format_biblio_record_entry",
1907     api_name  => "open-ils.search.biblio.record.print",
1908     signature => {
1909         desc   => 'Returns a printable version of the specified bib record',
1910         params => [
1911             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1912             { desc => 'Context library for holdings, if applicable' => 'number' },
1913             { desc => 'Sort order, if applicable' => 'string' },
1914             { desc => 'Definition Group Member id' => 'number' },
1915         ],
1916         return => {
1917             desc => q/An action_trigger.event object or error event./,
1918             type => 'object',
1919         }
1920     }
1921 );
1922 __PACKAGE__->register_method(
1923     method    => "format_biblio_record_entry",
1924     api_name  => "open-ils.search.biblio.record.email",
1925     signature => {
1926         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1927         params => [
1928             { desc => 'Authentication token',  type => 'string'},
1929             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1930             { desc => 'Context library for holdings, if applicable' => 'number' },
1931             { desc => 'Sort order, if applicable' => 'string' },
1932             { desc => 'Sort direction, if applicable' => 'string' },
1933             { desc => 'Definition Group Member id' => 'number' },
1934             { desc => 'Whether to bypass auth due to captcha' => 'bool' },
1935             { desc => 'Email address, if none for the user' => 'string' },
1936             { desc => 'Subject, if customized' => 'string' },
1937         ],
1938         return => {
1939             desc => q/Undefined on success, otherwise an error event./,
1940             type => 'object',
1941         }
1942     }
1943 );
1944
1945 sub format_biblio_record_entry {
1946     my($self, $conn, $arg1, $arg2, $arg3, $arg4, $arg5, $arg6, $captcha_pass, $email, $subject) = @_;
1947
1948     my $for_print = ($self->api_name =~ /print/);
1949     my $for_email = ($self->api_name =~ /email/);
1950     my $preview = ($self->api_name =~ /preview/);
1951
1952     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;
1953
1954     if ($for_print) {
1955         $bib_id = $arg1;
1956         $context_org = $arg2 || $U->get_org_tree->id;
1957         $holdings_context = $context_org;
1958         $bib_sort = $arg3 || 'author';
1959         $sort_dir = $arg4 || 'ascending';
1960         $group_member = $arg5;
1961         $e = new_editor(xact => 1);
1962     } elsif ($for_email) {
1963         $auth = $arg1;
1964         $bib_id = $arg2;
1965         $bib_sort = $arg4 || 'author';
1966         $sort_dir = $arg5 || 'ascending';
1967         $group_member = $arg6;
1968         $e = new_editor(authtoken => $auth, xact => 1);
1969         return $e->die_event unless $captcha_pass || $e->checkauth;
1970         $holdings_context = $arg3 || $U->get_org_tree->id;
1971         $context_org = $e->requestor ? $e->requestor->home_ou : $arg3;
1972         $email ||= $e->requestor ? $e->requestor->email : '';
1973     }
1974
1975     if ($group_member) {
1976         $group_member = $e->retrieve_action_trigger_event_def_group_member($group_member);
1977         if ($group_member and $U->is_true($group_member->holdings)) {
1978             $type = 'full';
1979         }
1980     }
1981
1982     $holdings_context = $e->retrieve_actor_org_unit($holdings_context);
1983
1984     my $bib_ids;
1985     if (ref $bib_id ne 'ARRAY') {
1986         $bib_ids = [ $bib_id ];
1987     } else {
1988         $bib_ids = $bib_id;
1989     }
1990
1991     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1992     $bucket->btype('temp');
1993     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1994     if ($for_email) {
1995         $bucket->owner($e->requestor || 1) 
1996     } else {
1997         $bucket->owner(1);
1998     }
1999     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2000
2001     for my $id (@$bib_ids) {
2002
2003         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2004
2005         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2006         $bucket_entry->target_biblio_record_entry($bib);
2007         $bucket_entry->bucket($bucket_obj->id);
2008         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2009     }
2010
2011     $e->commit;
2012
2013     my $usr_data = {
2014         type        => $type,
2015         email       => $email,
2016         subject     => $subject,
2017         context_org => $holdings_context->shortname,
2018         sort_by     => $bib_sort,
2019         sort_dir    => $sort_dir,
2020         preview     => $preview
2021     };
2022
2023     if ($for_print) {
2024
2025         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org, undef, [ $usr_data ]);
2026
2027     } elsif ($for_email) {
2028
2029         return $U->fire_object_event(undef, 'biblio.format.record_entry.email', [ $bucket ], $context_org, undef, [ $usr_data ])
2030             if ($preview);
2031
2032         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, $usr_data, 1);
2033     }
2034
2035     return undef;
2036 }
2037
2038
2039 __PACKAGE__->register_method(
2040     method   => "retrieve_all_copy_statuses",
2041     api_name => "open-ils.search.config.copy_status.retrieve.all"
2042 );
2043
2044 sub retrieve_all_copy_statuses {
2045     my( $self, $client ) = @_;
2046     return new_editor()->retrieve_all_config_copy_status();
2047 }
2048
2049
2050 __PACKAGE__->register_method(
2051     method   => "copy_counts_per_org",
2052     api_name => "open-ils.search.biblio.copy_counts.retrieve"
2053 );
2054
2055 __PACKAGE__->register_method(
2056     method   => "copy_counts_per_org",
2057     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2058 );
2059
2060 sub copy_counts_per_org {
2061     my( $self, $client, $record_id ) = @_;
2062
2063     warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2064
2065     my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2066     if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2067
2068     my $counts = $apputils->simple_scalar_request(
2069         "open-ils.storage", $method, $record_id );
2070
2071     $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2072     return $counts;
2073 }
2074
2075
2076 __PACKAGE__->register_method(
2077     method   => "copy_count_summary",
2078     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2079     notes    => "returns an array of these: "
2080               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2081               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2082 );
2083         
2084
2085 sub copy_count_summary {
2086     my( $self, $client, $rid, $org, $depth ) = @_;
2087     $org   ||= 1;
2088     $depth ||= 0;
2089     my $data = $U->storagereq(
2090         'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2091
2092     return [ sort {
2093         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2094         cmp
2095         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2096     } @$data ];
2097 }
2098
2099 __PACKAGE__->register_method(
2100     method   => "copy_location_count_summary",
2101     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2102     notes    => "returns an array of these: "
2103               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2104               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2105 );
2106
2107 sub copy_location_count_summary {
2108     my( $self, $client, $rid, $org, $depth ) = @_;
2109     $org   ||= 1;
2110     $depth ||= 0;
2111     my $data = $U->storagereq(
2112         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2113
2114     return [ sort {
2115         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2116         cmp
2117         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2118
2119         || $a->[4] cmp $b->[4]
2120     } @$data ];
2121 }
2122
2123 __PACKAGE__->register_method(
2124     method   => "copy_count_location_summary",
2125     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2126     notes    => "returns an array of these: "
2127               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2128               . "where statusx is a copy status name.  The statuses are sorted by ID."
2129 );
2130
2131 sub copy_count_location_summary {
2132     my( $self, $client, $rid, $org, $depth ) = @_;
2133     $org   ||= 1;
2134     $depth ||= 0;
2135     my $data = $U->storagereq(
2136         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2137     return [ sort {
2138         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2139         cmp
2140         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2141     } @$data ];
2142 }
2143
2144
2145 foreach (qw/open-ils.search.biblio.marc
2146             open-ils.search.biblio.marc.staff/)
2147 {
2148 __PACKAGE__->register_method(
2149     method    => "marc_search",
2150     api_name  => $_,
2151     signature => {
2152         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2153                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2154         params => [
2155             {
2156                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2157                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2158                 type => 'object'
2159             },
2160             {desc => 'timeout (optional)',  type => 'number'}
2161         ],
2162         return => {
2163             desc => 'Results object like: { "count": $i, "ids": [...] }',
2164             type => 'object'
2165         }
2166     }
2167 );
2168 }
2169
2170 =head3 open-ils.search.biblio.marc (arghash, timeout)
2171
2172 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2173
2174     searches: complex query object  (required)
2175     org_unit: The org ID to focus the search at
2176     depth   : The org depth     
2177     limit   : integer search limit      default: 10
2178     offset  : integer search offset     default:  0
2179     sort    : What field to sort the results on? [ author | title | pubdate ]
2180     sort_dir: In what direction do we sort? [ asc | desc ]
2181
2182 Additional keys to refine search criteria:
2183
2184     audience : Audience
2185     language : Language (code)
2186     lit_form : Literary form
2187     item_form: Item form
2188     item_type: Item type
2189     format   : The MARC format
2190
2191 Please note that the specific strings to be used in the "addtional keys" will be entirely
2192 dependent on your loaded data.  
2193
2194 All keys except "searches" are optional.
2195 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2196
2197 For example, an arg hash might look like:
2198
2199     $arghash = {
2200         searches => [
2201             {
2202                 term     => "harry",
2203                 restrict => [
2204                     {
2205                         tag => 245,
2206                         subfield => "a"
2207                     }
2208                     # ...
2209                 ]
2210             }
2211             # ...
2212         ],
2213         org_unit  => 1,
2214         limit     => 5,
2215         sort      => "author",
2216         item_type => "g"
2217     }
2218
2219 The arghash is eventually passed to the SRF call:
2220 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2221
2222 Presently, search uses the cache unconditionally.
2223
2224 =cut
2225
2226 # FIXME: that example above isn't actually tested.
2227 # FIXME: sort and limit added.  item_type not tested yet.
2228 # TODO: docache option?
2229 sub marc_search {
2230     my( $self, $conn, $args, $timeout ) = @_;
2231
2232     my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2233     $method .= ".staff" if $self->api_name =~ /staff/;
2234     $method .= ".atomic";
2235
2236     my $limit = $args->{limit} || 10;
2237     my $offset = $args->{offset} || 0;
2238
2239     # allow caller to pass in a call timeout since MARC searches
2240     # can take longer than the default 60-second timeout.  
2241     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2242     $timeout = 120 if !$timeout or $timeout > 300;
2243
2244     my @search;
2245     push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2246     my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2247
2248     my $recs = search_cache($ckey, $offset, $limit);
2249
2250     if(!$recs) {
2251
2252         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2253         my $req = $ses->request($method, %$args);
2254         my $resp = $req->recv($timeout);
2255
2256         if($resp and $recs = $resp->content) {
2257             put_cache($ckey, scalar(@$recs), $recs);
2258         } else {
2259             $recs = [];
2260         }
2261
2262         $ses->kill_me;
2263     }
2264
2265     my $count = 0;
2266     $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2267     my @recs = map { $_->[0] } @$recs;
2268
2269     return { ids => \@recs, count => $count };
2270 }
2271
2272
2273 foreach my $isbn_method (qw/
2274     open-ils.search.biblio.isbn
2275     open-ils.search.biblio.isbn.staff
2276 /) {
2277 __PACKAGE__->register_method(
2278     method    => "biblio_search_isbn",
2279     api_name  => $isbn_method,
2280     signature => {
2281         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2282         params => [
2283             {desc => 'ISBN', type => 'string'}
2284         ],
2285         return => {
2286             desc => 'Results object like: { "count": $i, "ids": [...] }',
2287             type => 'object'
2288         }
2289     }
2290 );
2291 }
2292
2293 sub biblio_search_isbn { 
2294     my( $self, $client, $isbn ) = @_;
2295     $logger->debug("Searching ISBN $isbn");
2296     # the previous implementation of this method was essentially unlimited,
2297     # so we will set our limit very high and let multiclass.query provide any
2298     # actual limit
2299     # XXX: if making this unlimited is deemed important, we might consider
2300     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2301     # which is functionally deprecated at this point, or a custom call to
2302     # 'open-ils.storage.biblio.multiclass.search_fts'
2303
2304     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2305     if ($self->api_name =~ m/.staff$/) {
2306         $isbn_method .= '.staff';
2307     }
2308
2309     my $method = $self->method_lookup($isbn_method);
2310     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2311     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2312     return { ids => \@recs, count => $search_result->{'count'} };
2313 }
2314
2315 __PACKAGE__->register_method(
2316     method   => "biblio_search_isbn_batch",
2317     api_name => "open-ils.search.biblio.isbn_list",
2318 );
2319
2320 # XXX: see biblio_search_isbn() for note concerning 'limit'
2321 sub biblio_search_isbn_batch { 
2322     my( $self, $client, $isbn_list ) = @_;
2323     $logger->debug("Searching ISBNs @$isbn_list");
2324     my @recs = (); my %rec_set = ();
2325     my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2326     foreach my $isbn ( @$isbn_list ) {
2327         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2328         my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2329         foreach my $rec (@recs_subset) {
2330             if (! $rec_set{ $rec }) {
2331                 $rec_set{ $rec } = 1;
2332                 push @recs, $rec;
2333             }
2334         }
2335     }
2336     return { ids => \@recs, count => scalar(@recs) };
2337 }
2338
2339 foreach my $issn_method (qw/
2340     open-ils.search.biblio.issn
2341     open-ils.search.biblio.issn.staff
2342 /) {
2343 __PACKAGE__->register_method(
2344     method   => "biblio_search_issn",
2345     api_name => $issn_method,
2346     signature => {
2347         desc   => 'Retrieve biblio IDs for a given ISSN',
2348         params => [
2349             {desc => 'ISBN', type => 'string'}
2350         ],
2351         return => {
2352             desc => 'Results object like: { "count": $i, "ids": [...] }',
2353             type => 'object'
2354         }
2355     }
2356 );
2357 }
2358
2359 sub biblio_search_issn { 
2360     my( $self, $client, $issn ) = @_;
2361     $logger->debug("Searching ISSN $issn");
2362     # the previous implementation of this method was essentially unlimited,
2363     # so we will set our limit very high and let multiclass.query provide any
2364     # actual limit
2365     # XXX: if making this unlimited is deemed important, we might consider
2366     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2367     # which is functionally deprecated at this point, or a custom call to
2368     # 'open-ils.storage.biblio.multiclass.search_fts'
2369
2370     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2371     if ($self->api_name =~ m/.staff$/) {
2372         $issn_method .= '.staff';
2373     }
2374
2375     my $method = $self->method_lookup($issn_method);
2376     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2377     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2378     return { ids => \@recs, count => $search_result->{'count'} };
2379 }
2380
2381
2382 __PACKAGE__->register_method(
2383     method    => "fetch_mods_by_copy",
2384     api_name  => "open-ils.search.biblio.mods_from_copy",
2385     argc      => 1,
2386     signature => {
2387         desc    => 'Retrieve MODS record given an attached copy ID',
2388         params  => [
2389             { desc => 'Copy ID', type => 'number' }
2390         ],
2391         returns => {
2392             desc => 'MODS record, event on error or uncataloged item'
2393         }
2394     }
2395 );
2396
2397 sub fetch_mods_by_copy {
2398     my( $self, $client, $copyid ) = @_;
2399     my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2400     return $evt if $evt;
2401     return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2402     return $apputils->record_to_mvr($record);
2403 }
2404
2405
2406 # -------------------------------------------------------------------------------------
2407
2408 __PACKAGE__->register_method(
2409     method   => "cn_browse",
2410     api_name => "open-ils.search.callnumber.browse.target",
2411     notes    => "Starts a callnumber browse"
2412 );
2413
2414 __PACKAGE__->register_method(
2415     method   => "cn_browse",
2416     api_name => "open-ils.search.callnumber.browse.page_up",
2417     notes    => "Returns the previous page of callnumbers",
2418 );
2419
2420 __PACKAGE__->register_method(
2421     method   => "cn_browse",
2422     api_name => "open-ils.search.callnumber.browse.page_down",
2423     notes    => "Returns the next page of callnumbers",
2424 );
2425
2426
2427 # RETURNS array of arrays like so: label, owning_lib, record, id
2428 sub cn_browse {
2429     my( $self, $client, @params ) = @_;
2430     my $method;
2431
2432     $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2433         if( $self->api_name =~ /target/ );
2434     $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2435         if( $self->api_name =~ /page_up/ );
2436     $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2437         if( $self->api_name =~ /page_down/ );
2438
2439     return $apputils->simplereq( 'open-ils.storage', $method, @params );
2440 }
2441 # -------------------------------------------------------------------------------------
2442
2443 __PACKAGE__->register_method(
2444     method        => "fetch_cn",
2445     api_name      => "open-ils.search.callnumber.retrieve",
2446     authoritative => 1,
2447     notes         => "retrieves a callnumber based on ID",
2448 );
2449
2450 sub fetch_cn {
2451     my( $self, $client, $id ) = @_;
2452
2453     my $e = new_editor();
2454     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2455     return $evt if $evt;
2456     return $cn;
2457 }
2458
2459 __PACKAGE__->register_method(
2460     method        => "fetch_fleshed_cn",
2461     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2462     authoritative => 1,
2463     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2464 );
2465
2466 sub fetch_fleshed_cn {
2467     my( $self, $client, $id ) = @_;
2468
2469     my $e = new_editor();
2470     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2471     return $evt if $evt;
2472     return $cn;
2473 }
2474
2475
2476 __PACKAGE__->register_method(
2477     method    => "fetch_copy_by_cn",
2478     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2479     signature => q/
2480         Returns an array of copy ID's by callnumber ID
2481         @param cnid The callnumber ID
2482         @return An array of copy IDs
2483     /
2484 );
2485
2486 sub fetch_copy_by_cn {
2487     my( $self, $conn, $cnid ) = @_;
2488     return $U->cstorereq(
2489         'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2490         { call_number => $cnid, deleted => 'f' } );
2491 }
2492
2493 __PACKAGE__->register_method(
2494     method    => 'fetch_cn_by_info',
2495     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2496     signature => q/
2497         @param label The callnumber label
2498         @param record The record the cn is attached to
2499         @param org The owning library of the cn
2500         @return The callnumber object
2501     /
2502 );
2503
2504
2505 sub fetch_cn_by_info {
2506     my( $self, $conn, $label, $record, $org ) = @_;
2507     return $U->cstorereq(
2508         'open-ils.cstore.direct.asset.call_number.search',
2509         { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2510 }
2511
2512
2513
2514 __PACKAGE__->register_method(
2515     method   => 'bib_extras',
2516     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2517     ctype => 'lit_form'
2518 );
2519 __PACKAGE__->register_method(
2520     method   => 'bib_extras',
2521     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2522     ctype => 'item_form'
2523 );
2524 __PACKAGE__->register_method(
2525     method   => 'bib_extras',
2526     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2527     ctype => 'item_type',
2528 );
2529 __PACKAGE__->register_method(
2530     method   => 'bib_extras',
2531     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2532     ctype => 'bib_level'
2533 );
2534 __PACKAGE__->register_method(
2535     method   => 'bib_extras',
2536     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2537     ctype => 'audience'
2538 );
2539
2540 sub bib_extras {
2541     my $self = shift;
2542     $logger->warn("deprecation warning: " .$self->api_name);
2543
2544     my $e = new_editor();
2545
2546     my $ctype = $self->{ctype};
2547     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2548
2549     my @objs;
2550     for my $ccvm (@$ccvms) {
2551         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2552         $obj->value($ccvm->value);
2553         $obj->code($ccvm->code);
2554         $obj->description($ccvm->description) if $obj->can('description');
2555         push(@objs, $obj);
2556     }
2557
2558     return \@objs;
2559 }
2560
2561
2562
2563 __PACKAGE__->register_method(
2564     method    => 'fetch_slim_record',
2565     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2566     signature => {
2567         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2568         params => [
2569             { desc => 'Array of Record IDs', type => 'array' }
2570         ],
2571         return => { 
2572             desc => 'Array of biblio records, event on error'
2573         }
2574     }
2575 );
2576
2577 sub fetch_slim_record {
2578     my( $self, $conn, $ids ) = @_;
2579
2580     my $editor = new_editor();
2581     my @res;
2582     for( @$ids ) {
2583         return $editor->event unless
2584             my $r = $editor->retrieve_biblio_record_entry($_);
2585         $r->clear_marc;
2586         push(@res, $r);
2587     }
2588     return \@res;
2589 }
2590
2591 __PACKAGE__->register_method(
2592     method    => 'rec_hold_parts',
2593     api_name  => 'open-ils.search.biblio.record_hold_parts',
2594     signature => q/
2595        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2596     /
2597 );
2598
2599 sub rec_hold_parts {
2600     my( $self, $conn, $args ) = @_;
2601
2602     my $rec        = $$args{record};
2603     my $mrec       = $$args{metarecord};
2604     my $pickup_lib = $$args{pickup_lib};
2605     my $e = new_editor();
2606
2607     my $query = {
2608         select => {bmp => ['id', 'label']},
2609         from => 'bmp',
2610         where => {
2611             id => {
2612                 in => {
2613                     select => {'acpm' => ['part']},
2614                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2615                     where => {
2616                         '+acp' => {'deleted' => 'f'},
2617                         '+bre' => {id => $rec}
2618                     },
2619                     distinct => 1,
2620                 }
2621             },
2622             deleted => 'f'
2623         },
2624         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2625     };
2626
2627     if(defined $pickup_lib) {
2628         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2629         if($hard_boundary) {
2630             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2631             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2632         }
2633     }
2634
2635     return $e->json_query($query);
2636 }
2637
2638
2639
2640
2641 __PACKAGE__->register_method(
2642     method    => 'rec_to_mr_rec_descriptors',
2643     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2644     signature => q/
2645         specialized method...
2646         Given a biblio record id or a metarecord id, 
2647         this returns a list of metabib.record_descriptor
2648         objects that live within the same metarecord
2649         @param args Object of args including:
2650     /
2651 );
2652
2653 sub rec_to_mr_rec_descriptors {
2654     my( $self, $conn, $args ) = @_;
2655
2656     my $rec        = $$args{record};
2657     my $mrec       = $$args{metarecord};
2658     my $item_forms = $$args{item_forms};
2659     my $item_types = $$args{item_types};
2660     my $item_lang  = $$args{item_lang};
2661     my $pickup_lib = $$args{pickup_lib};
2662
2663     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2664
2665     my $e = new_editor();
2666     my $recs;
2667
2668     if( !$mrec ) {
2669         my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2670         return $e->event unless @$map;
2671         $mrec = $$map[0]->metarecord;
2672     }
2673
2674     $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2675     return $e->event unless @$recs;
2676
2677     my @recs = map { $_->source } @$recs;
2678     my $search = { record => \@recs };
2679     $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2680     $search->{item_type} = $item_types if $item_types and @$item_types;
2681     $search->{item_lang} = $item_lang  if $item_lang;
2682
2683     my $desc = $e->search_metabib_record_descriptor($search);
2684
2685     my $query = {
2686         distinct => 1,
2687         select   => { 'bre' => ['id'] },
2688         from     => {
2689             'bre' => {
2690                 'acn' => {
2691                     'join' => {
2692                         'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2693                       }
2694                   }
2695              }
2696         },
2697         where => {
2698             '+bre' => { id => \@recs },
2699             '+acp' => {
2700                 holdable => 't',
2701                 deleted  => 'f'
2702             },
2703             "+ccs" => { holdable => 't' },
2704             "+acpl" => { holdable => 't', deleted => 'f' }
2705         }
2706     };
2707
2708     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2709         my $orgs = $e->json_query(
2710             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2711         ) or return $e->die_event;
2712
2713         $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2714     }
2715
2716     my $good_records = $e->json_query($query) or return $e->die_event;
2717
2718     my @keep;
2719     for my $d (@$desc) {
2720         if ( grep { $d->record == $_->{id} } @$good_records ) {
2721             push @keep, $d;
2722         }
2723     }
2724
2725     $desc = \@keep;
2726
2727     return { metarecord => $mrec, descriptors => $desc };
2728 }
2729
2730
2731 __PACKAGE__->register_method(
2732     method   => 'fetch_age_protect',
2733     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2734 );
2735
2736 sub fetch_age_protect {
2737     return new_editor()->retrieve_all_config_rule_age_hold_protect();
2738 }
2739
2740
2741 __PACKAGE__->register_method(
2742     method   => 'copies_by_cn_label',
2743     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2744 );
2745
2746 __PACKAGE__->register_method(
2747     method   => 'copies_by_cn_label',
2748     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2749 );
2750
2751 sub copies_by_cn_label {
2752     my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2753     my $e = new_editor();
2754     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2755     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2756     my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2757     return [] unless @$cns;
2758
2759     # show all non-deleted copies in the staff client ...
2760     if ($self->api_name =~ /staff$/o) {
2761         return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2762     }
2763
2764     # ... otherwise, grab the copies ...
2765     my $copies = $e->search_asset_copy(
2766         [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2767           {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2768         ]
2769     );
2770
2771     # ... and test for location and status visibility
2772     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2773 }
2774
2775 __PACKAGE__->register_method(
2776     method   => 'bib_copies',
2777     api_name => 'open-ils.search.bib.copies',
2778     stream => 1
2779 );
2780 __PACKAGE__->register_method(
2781     method   => 'bib_copies',
2782     api_name => 'open-ils.search.bib.copies.staff',
2783     stream => 1
2784 );
2785
2786 sub bib_copies {
2787     my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2788     my $is_staff = ($self->api_name =~ /staff/);
2789
2790     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2791     my $req = $cstore->request(
2792         'open-ils.cstore.json_query', mk_copy_query(
2793         $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2794
2795     my $resp;
2796     while ($resp = $req->recv) {
2797         $client->respond($resp->content); 
2798     }
2799
2800     return undef;
2801 }
2802
2803 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2804 # Refactor to share
2805 sub mk_copy_query {
2806     my $rec_id = shift;
2807     my $org = shift;
2808     my $depth = shift;
2809     my $copy_limit = shift;
2810     my $copy_offset = shift;
2811     my $pref_ou = shift;
2812     my $is_staff = shift;
2813
2814     my $query = $U->basic_opac_copy_query(
2815         $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2816     );
2817
2818     if ($org) { # TODO: root org test
2819         # no need to add the org join filter if we're not actually filtering
2820         $query->{from}->{acp}->[1] = { aou => {
2821             fkey => 'circ_lib',
2822             field => 'id',
2823             filter => {
2824                 id => {
2825                     in => {
2826                         select => {aou => [{
2827                             column => 'id', 
2828                             transform => 'actor.org_unit_descendants',
2829                             result_field => 'id', 
2830                             params => [$depth]
2831                         }]},
2832                         from => 'aou',
2833                         where => {id => $org}
2834                     }
2835                 }
2836             }
2837         }};
2838     };
2839
2840     # Unsure if we want these in the shared function, leaving here for now
2841     unshift(@{$query->{order_by}},
2842         { class => "aou", field => 'id',
2843           transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2844         }
2845     );
2846     push(@{$query->{order_by}},
2847         { class => "acp", field => 'id',
2848           transform => 'evergreen.rank_cp'
2849         }
2850     );
2851
2852     return $query;
2853 }
2854
2855
2856 __PACKAGE__->register_method(
2857     method    => 'catalog_record_summary',
2858     api_name  => 'open-ils.search.biblio.record.catalog_summary',
2859     stream    => 1,
2860     max_bundle_count => 1,
2861     signature => {
2862         desc   => 'Stream of record data suitable for catalog display',
2863         params => [
2864             {desc => 'Context org unit ID', type => 'number'},
2865             {desc => 'Array of Record IDs', type => 'array'}
2866         ],
2867         return => { 
2868             desc => q/
2869                 Stream of record summary objects including id, record,
2870                 hold_count, copy_counts, display (metabib display
2871                 fields), attributes (metabib record attrs), plus
2872                 metabib_id and metabib_records for the metabib variant.
2873             /
2874         }
2875     }
2876 );
2877 __PACKAGE__->register_method(
2878     method    => 'catalog_record_summary',
2879     api_name  => 'open-ils.search.biblio.record.catalog_summary.staff',
2880     stream    => 1,
2881     max_bundle_count => 1,
2882     signature => q/see open-ils.search.biblio.record.catalog_summary/
2883 );
2884 __PACKAGE__->register_method(
2885     method    => 'catalog_record_summary',
2886     api_name  => 'open-ils.search.biblio.metabib.catalog_summary',
2887     stream    => 1,
2888     max_bundle_count => 1,
2889     signature => q/see open-ils.search.biblio.record.catalog_summary/
2890 );
2891
2892 __PACKAGE__->register_method(
2893     method    => 'catalog_record_summary',
2894     api_name  => 'open-ils.search.biblio.metabib.catalog_summary.staff',
2895     stream    => 1,
2896     max_bundle_count => 1,
2897     signature => q/see open-ils.search.biblio.record.catalog_summary/
2898 );
2899
2900
2901 sub catalog_record_summary {
2902     my ($self, $client, $org_id, $record_ids) = @_;
2903     my $e = new_editor();
2904
2905     my $is_meta = ($self->api_name =~ /metabib/);
2906     my $is_staff = ($self->api_name =~ /staff/);
2907
2908     my $holds_method = $is_meta ? 
2909         'open-ils.circ.mmr.holds.count' : 
2910         'open-ils.circ.bre.holds.count';
2911
2912     my $copy_method = $is_meta ? 
2913         'open-ils.search.biblio.metarecord.copy_count':
2914         'open-ils.search.biblio.record.copy_count';
2915
2916     $copy_method .= '.staff' if $is_staff;
2917
2918     $copy_method = $self->method_lookup($copy_method); # local method
2919
2920     for my $rec_id (@$record_ids) {
2921
2922         my $response = $is_meta ? 
2923             get_one_metarecord_summary($e, $rec_id) :
2924             get_one_record_summary($e, $rec_id);
2925
2926         ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id);
2927
2928         $response->{hold_count} = 
2929             $U->simplereq('open-ils.circ', $holds_method, $rec_id);
2930
2931         $client->respond($response);
2932     }
2933
2934     return undef;
2935 }
2936
2937 # Start with a bib summary and augment the data with additional
2938 # metarecord content.
2939 sub get_one_metarecord_summary {
2940     my ($e, $rec_id) = @_;
2941
2942     my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {};
2943     my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id});
2944
2945     my $bre_id = $meta->master_record; 
2946
2947     my $response = get_one_record_summary($e, $bre_id);
2948
2949     $response->{metabib_id} = $rec_id;
2950     $response->{metabib_records} = [map {$_->source} @$maps];
2951
2952     my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps;
2953
2954     # Augment the record attributes with those of all of the records
2955     # linked to this metarecord.
2956     if (@other_bibs) {
2957         my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs});
2958
2959         my $attributes = $response->{attributes};
2960
2961         for my $attr (@$attrs) {
2962             $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr};
2963             push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes
2964                 unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}};
2965         }
2966     }
2967
2968     return $response;
2969 }
2970
2971 sub get_one_record_summary {
2972     my ($e, $rec_id) = @_;
2973
2974     my $bre = $e->retrieve_biblio_record_entry([$rec_id, {
2975         flesh => 1,
2976         flesh_fields => {
2977             bre => [qw/compressed_display_entries mattrs creator editor/]
2978         }
2979     }]) or return {};
2980
2981     # Compressed display fields are pachaged as JSON
2982     my $display = {};
2983     $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value)
2984         foreach @{$bre->compressed_display_entries};
2985
2986     # Create an object of 'mraf' attributes.
2987     # Any attribute can be multi so dedupe and array-ify all of them.
2988     my $attributes = {};
2989     for my $attr (@{$bre->mattrs}) {
2990         $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr};
2991         $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes
2992     }
2993     $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes;
2994
2995     # clear bulk
2996     $bre->clear_marc;
2997     $bre->clear_mattrs;
2998     $bre->clear_compressed_display_entries;
2999
3000     return {
3001         id => $rec_id,
3002         record => $bre,
3003         display => $display,
3004         attributes => $attributes
3005     };
3006 }
3007
3008
3009 1;
3010