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