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