]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
LP1615805 No inputs after submit in patron search (AngularJS)
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Search / Biblio.pm
1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
4
5
6 use OpenSRF::Utils::JSON;
7 use OpenILS::Utils::Fieldmapper;
8 use OpenILS::Utils::ModsParser;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenSRF::Utils::Cache;
12 use Encode;
13 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 ($class, $term, $field_list) = one_class_multi_term($global_summary->{query_struct});
1312     if ($class and $term) { # we meet the current "can suggest" criteria, check for suggestions!
1313         my $editor = new_editor();
1314         my $class_settings = $editor->retrieve_config_metabib_class($class);
1315         $field_list ||= [];
1316
1317         if ( # search did not provide enough hits and settings
1318              # for this class want more than 0 suggestions
1319             $global_summary->{visible} <= $class_settings->low_result_threshold
1320             and $class_settings->max_suggestions != 0
1321         ) {
1322             my $suggestion_verbosity = $class_settings->symspell_suggestion_verbosity;
1323             if ($class_settings->max_suggestions == -1) { # special value that means "only best suggestion, and not always"
1324                 $class_settings->max_suggestions(1);
1325                 $suggestion_verbosity = 0;
1326             }
1327
1328             my $suggs = $editor->json_query({
1329                 from  => [
1330                     'search.symspell_suggest',
1331                         $term, $class, '{'.join($field_list).'}',
1332                         undef, # max edit distance per word, just get the database setting
1333                         $suggestion_verbosity
1334                 ]
1335             });
1336
1337             @$suggs = sort {
1338                 $$a{lev_distance} <=> $$b{lev_distance}
1339                 || (
1340                     $$b{pg_trgm_sim} * $class_settings->pg_trgm_weight
1341                     + $$b{soundex_sim} * $class_settings->soundex_weight
1342                     + $$b{qwerty_kb_match} * $class_settings->keyboard_distance_weight
1343                         <=>
1344                     $$a{pg_trgm_sim} * $class_settings->pg_trgm_weight
1345                     + $$a{soundex_sim} * $class_settings->soundex_weight
1346                     + $$a{qwerty_kb_match} * $class_settings->keyboard_distance_weight
1347                 )
1348                 || abs($$b{suggestion_count}) <=> abs($$a{suggestion_count})
1349             } grep  { $$_{lev_distance} != 0 || $$_{suggestion_count} < 0 } @$suggs;
1350
1351             if (@$suggs) {
1352                 $global_summary->{suggestions}{'one_class_multi_term'} = {
1353                     class       => $class,
1354                     term        => $term,
1355                     suggestions  => [ splice @$suggs, 0, $class_settings->max_suggestions ]
1356                 };
1357             }
1358         }
1359     }
1360
1361     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1362
1363     $conn->respond_complete(
1364         {
1365             global_summary    => $global_summary,
1366             count             => $global_summary->{visible},
1367             core_limit        => $search_hash->{core_limit},
1368             superpage         => $page,
1369             superpage_size    => $search_hash->{check_limit},
1370             superpage_summary => $current_page_summary,
1371             facet_key         => $facet_key,
1372             ids               => \@results
1373         }
1374     );
1375     $cache->{memcache}->decr($counter_key);
1376
1377     $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1378
1379     return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1380 }
1381
1382 sub one_class_multi_term {
1383     my $qstruct = shift;
1384     my $fields = shift;
1385     my $node = $$qstruct{children};
1386
1387     my $class = undef;
1388     my $term = '';
1389     if ($fields) {
1390         if ($$node{fields} and @{$$node{fields}} > 0) {
1391             return (undef,undef,undef) if (join(',', @{$$node{fields}}) ne join(',', @$fields));
1392         }
1393     } elsif ($$node{fields}) {
1394         $fields = [ @{$$node{fields}} ];
1395     }
1396
1397
1398     # may relax this...
1399     return (undef,undef,undef) if ($$node{'|'}
1400         # or ($$node{modifiers} and @{$$node{modifiers}} > 0)
1401         # or ($$node{filters} and @{$$node{filters}} > 0)
1402     );
1403
1404     for my $kid (@{$$node{'&'}}) {
1405         my ($subclass, $subterm);
1406         if ($$kid{type} eq 'query_plan') {
1407             ($subclass, $subterm) = one_class_multi_term($kid, $fields);
1408             return (undef,undef,undef) if ($class and $subclass and $class ne $subclass);
1409             $class = $subclass;
1410             $term .= ' ' if $term;
1411             $term .= $subterm if $subterm;
1412         } elsif ($$kid{type} eq 'node') {
1413             $subclass = $$kid{class};
1414             return (undef,undef,undef) if ($class and $subclass and $class ne $subclass);
1415             $class = $subclass;
1416             ($subclass, $subterm) = one_class_multi_term($kid, $fields);
1417             return (undef,undef,undef) if ($subclass and $class ne $subclass);
1418             $term .= ' ' if $term;
1419             $term .= $subterm if $subterm;
1420         } elsif ($$kid{type} eq 'atom') {
1421             $term .= ' ' if $term;
1422             if ($$kid{content} !~ /\s+/ and $$kid{prefix} =~ /^-/) {
1423                 # only quote negated multi-word phrases, not negated single words
1424                 $$kid{prefix} = '-';
1425                 $$kid{suffix} = '';
1426             }
1427             $term .= $$kid{prefix}.$$kid{content}.$$kid{suffix};
1428         }
1429     }
1430
1431     return ($class, $term, $fields);
1432 }
1433
1434 sub fetch_display_fields {
1435     my $self = shift;
1436     my $conn = shift;
1437     my $highlight_map = shift;
1438     my @records = @_;
1439
1440     unless (@records) {
1441         $conn->respond_complete;
1442         return;
1443     }
1444
1445     my $e = new_editor();
1446
1447     for my $record ( @records ) {
1448         next unless ($record && $highlight_map);
1449         $conn->respond(
1450             $e->json_query(
1451                 {from => ['search.highlight_display_fields', $record, $highlight_map]}
1452             )
1453         );
1454     }
1455
1456     return undef;
1457 }
1458 __PACKAGE__->register_method(
1459     method    => 'fetch_display_fields',
1460     api_name  => 'open-ils.search.fetch.metabib.display_field.highlight',
1461     stream   => 1
1462 );
1463
1464
1465 sub tag_circulated_records {
1466     my ($auth, $results, $metabib) = @_;
1467     my $e = new_editor(authtoken => $auth);
1468     return $results unless $e->checkauth;
1469
1470     my $query = {
1471         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1472         from     => { auch => { acp => { join => 'acn' }} }, 
1473         where    => { usr => $e->requestor->id },
1474         distinct => 1
1475     };
1476
1477     if ($metabib) {
1478         $query = {
1479             select   => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1480             from     => 'mmrsm',
1481             where    => { source => { in => $query } },
1482             distinct => 1
1483         };
1484     }
1485
1486     # Give me the distinct set of bib records that exist in the user's visible circulation history
1487     my $circ_recs = $e->json_query( $query );
1488
1489     # if the record appears in the circ history, push a 1 onto 
1490     # the rec array structure to indicate truthiness
1491     for my $rec (@$results) {
1492         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1493     }
1494
1495     $results
1496 }
1497
1498 # creates a unique token to represent the query in the cache
1499 sub search_cache_key {
1500     my $method = shift;
1501     my $search_hash = shift;
1502     my @sorted;
1503     for my $key (sort keys %$search_hash) {
1504         push(@sorted, ($key => $$search_hash{$key})) 
1505             unless $key eq 'limit'  or 
1506                    $key eq 'offset' or 
1507                    $key eq 'skip_check';
1508     }
1509     my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1510     return $pfx . md5_hex($method . $s);
1511 }
1512
1513 sub retrieve_cached_facets {
1514     my $self   = shift;
1515     my $client = shift;
1516     my $key    = shift;
1517     my $limit    = shift;
1518
1519     return undef unless ($key and $key =~ /_facets$/);
1520
1521     eval {
1522         local $SIG{ALRM} = sub {die};
1523         alarm(10); # we'll sleep for as much as 10s
1524         do {
1525             die if $cache->get_cache($key . '_COMPLETE');
1526         } while (sleep(0.05));
1527         alarm(0);
1528     };
1529     alarm(0);
1530
1531     my $blob = $cache->get_cache($key) || {};
1532
1533     my $facets = {};
1534     if ($limit) {
1535        for my $f ( keys %$blob ) {
1536             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1537             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1538             for my $s ( @sorted ) {
1539                 my ($k) = keys(%$s);
1540                 my ($v) = values(%$s);
1541                 $$facets{$f}{$k} = $v;
1542             }
1543         }
1544     } else {
1545         $facets = $blob;
1546     }
1547
1548     return $facets;
1549 }
1550
1551 __PACKAGE__->register_method(
1552     method   => "retrieve_cached_facets",
1553     api_name => "open-ils.search.facet_cache.retrieve",
1554     signature => {
1555         desc   => 'Returns facet data derived from a specific search based on a key '.
1556                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1557         params => [
1558             {
1559                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1560                 type => 'string',
1561             }
1562         ],
1563         return => {
1564             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1565                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1566                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1567                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1568             type => 'object',
1569         }
1570     }
1571 );
1572
1573
1574 sub cache_facets {
1575     # add facets for this search to the facet cache
1576     my($key, $results, $metabib, $ignore) = @_;
1577     my $data = $cache->get_cache($key);
1578     $data ||= {};
1579
1580     return undef unless (@$results);
1581
1582     my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1583                                    : 'search.facets_for_record_set';
1584     my $results_str = '{' . join(',', @$results) . '}';
1585     my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1586                                   : '{}';
1587     my $query = {   
1588         from => [ $facets_function, $ignore_str, $results_str ]
1589     };
1590
1591     my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1592
1593     for my $facet (@$facets) {
1594         next unless ($facet->{value});
1595         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1596     }
1597
1598     $logger->info("facet compilation: cached with key=$key");
1599
1600     $cache->put_cache($key, $data, $cache_timeout);
1601     $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
1602 }
1603
1604 sub cache_staged_search_page {
1605     # puts this set of results into the cache
1606     my($key, $page, $summary, $results) = @_;
1607     my $data = $cache->get_cache($key);
1608     $data ||= {};
1609     $data->{$page} = {
1610         summary => $summary,
1611         results => $results
1612     };
1613
1614     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1615         ($summary->{estimated_hit_count} || "none") .
1616         ", visible=" . ($summary->{visible} || "none")
1617     );
1618
1619     $cache->put_cache($key, $data, $cache_timeout);
1620 }
1621
1622 sub search_cache {
1623
1624     my $key     = shift;
1625     my $offset  = shift;
1626     my $limit   = shift;
1627     my $start   = $offset;
1628     my $end     = $offset + $limit - 1;
1629
1630     $logger->debug("searching cache for $key : $start..$end\n");
1631
1632     return undef unless $cache;
1633     my $data = $cache->get_cache($key);
1634
1635     return undef unless $data;
1636
1637     my $count = $data->[0];
1638     $data = $data->[1];
1639
1640     return undef unless $offset < $count;
1641
1642     my @result;
1643     for( my $i = $offset; $i <= $end; $i++ ) {
1644         last unless my $d = $$data[$i];
1645         push( @result, $d );
1646     }
1647
1648     $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1649
1650     return \@result;
1651 }
1652
1653
1654 sub put_cache {
1655     my( $key, $count, $data ) = @_;
1656     return undef unless $cache;
1657     $logger->debug("search_cache putting ".
1658         scalar(@$data)." items at key $key with timeout $cache_timeout");
1659     $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1660 }
1661
1662
1663 __PACKAGE__->register_method(
1664     method   => "biblio_mrid_to_modsbatch_batch",
1665     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1666 );
1667
1668 sub biblio_mrid_to_modsbatch_batch {
1669     my( $self, $client, $mrids) = @_;
1670     # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1671     my @mods;
1672     my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1673     for my $id (@$mrids) {
1674         next unless defined $id;
1675         my ($m) = $method->run($id);
1676         push @mods, $m;
1677     }
1678     return \@mods;
1679 }
1680
1681
1682 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1683              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1684     {
1685     __PACKAGE__->register_method(
1686         method    => "biblio_mrid_to_modsbatch",
1687         api_name  => $_,
1688         signature => {
1689             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1690                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1691             params => [
1692                 { desc => 'Metarecord ID', type => 'number' },
1693                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1694             ],
1695             return => {
1696                 desc => 'MVR Object, event on error',
1697             }
1698         }
1699     );
1700 }
1701
1702 sub biblio_mrid_to_modsbatch {
1703     my( $self, $client, $mrid, $args) = @_;
1704
1705     # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1706
1707     my ($mr, $evt) = _grab_metarecord($mrid);
1708     return $evt unless $mr;
1709
1710     my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1711               biblio_mrid_make_modsbatch($self, $client, $mr);
1712
1713     return $mvr unless ref($args);  
1714
1715     # Here we find the lead record appropriate for the given filters 
1716     # and use that for the title and author of the metarecord
1717     my $format = $$args{format};
1718     my $org    = $$args{org};
1719     my $depth  = $$args{depth};
1720
1721     return $mvr unless $format or $org or $depth;
1722
1723     my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1724     $method = "$method.staff" if $self->api_name =~ /staff/o; 
1725
1726     my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1727
1728     if( my $mods = $U->record_to_mvr($rec) ) {
1729
1730         $mvr->title( $mods->title );
1731         $mvr->author($mods->author);
1732         $logger->debug("mods_slim updating title and ".
1733             "author in mvr with ".$mods->title." : ".$mods->author);
1734     }
1735
1736     return $mvr;
1737 }
1738
1739 # converts a metarecord to an mvr
1740 sub _mr_to_mvr {
1741     my $mr = shift;
1742     my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1743     return Fieldmapper::metabib::virtual_record->new($perl);
1744 }
1745
1746 # checks to see if a metarecord has mods, if so returns true;
1747
1748 __PACKAGE__->register_method(
1749     method   => "biblio_mrid_check_mvr",
1750     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1751     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1752               . "if the metarecord already has an mvr associated with it."
1753 );
1754
1755 sub biblio_mrid_check_mvr {
1756     my( $self, $client, $mrid ) = @_;
1757     my $mr; 
1758
1759     my $evt;
1760     if(ref($mrid)) { $mr = $mrid; } 
1761     else { ($mr, $evt) = _grab_metarecord($mrid); }
1762     return $evt if $evt;
1763
1764     # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1765
1766     return _mr_to_mvr($mr) if $mr->mods();
1767     return undef;
1768 }
1769
1770 sub _grab_metarecord {
1771     my $mrid = shift;
1772     my $e = new_editor();
1773     my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1774     return ($mr);
1775 }
1776
1777
1778 __PACKAGE__->register_method(
1779     method   => "biblio_mrid_make_modsbatch",
1780     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1781     notes    => "Takes either a metarecord ID or a metarecord object. "
1782               . "Forces the creations of an mvr for the given metarecord. "
1783               . "The created mvr is returned."
1784 );
1785
1786 sub biblio_mrid_make_modsbatch {
1787     my( $self, $client, $mrid ) = @_;
1788
1789     my $e = new_editor();
1790
1791     my $mr;
1792     if( ref($mrid) ) {
1793         $mr = $mrid;
1794         $mrid = $mr->id;
1795     } else {
1796         $mr = $e->retrieve_metabib_metarecord($mrid) 
1797             or return $e->event;
1798     }
1799
1800     my $masterid = $mr->master_record;
1801     $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1802
1803     my $ids = $U->storagereq(
1804         'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1805     return undef unless @$ids;
1806
1807     my $master = $e->retrieve_biblio_record_entry($masterid)
1808         or return $e->event;
1809
1810     # start the mods batch
1811     my $u = OpenILS::Utils::ModsParser->new();
1812     $u->start_mods_batch( $master->marc );
1813
1814     # grab all of the sub-records and shove them into the batch
1815     my @ids = grep { $_ ne $masterid } @$ids;
1816     #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1817
1818     my $subrecs = [];
1819     if(@$ids) {
1820         for my $i (@$ids) {
1821             my $r = $e->retrieve_biblio_record_entry($i);
1822             push( @$subrecs, $r ) if $r;
1823         }
1824     }
1825
1826     for(@$subrecs) {
1827         $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1828         $u->push_mods_batch( $_->marc ) if $_->marc;
1829     }
1830
1831
1832     # finish up and send to the client
1833     my $mods = $u->finish_mods_batch();
1834     $mods->doc_id($mrid);
1835     $client->respond_complete($mods);
1836
1837
1838     # now update the mods string in the db
1839     my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1840     $mr->mods($string);
1841
1842     $e = new_editor(xact => 1);
1843     $e->update_metabib_metarecord($mr) 
1844         or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1845     $e->finish;
1846
1847     return undef;
1848 }
1849
1850
1851 # converts a mr id into a list of record ids
1852
1853 foreach (qw/open-ils.search.biblio.metarecord_to_records
1854             open-ils.search.biblio.metarecord_to_records.staff/)
1855 {
1856     __PACKAGE__->register_method(
1857         method    => "biblio_mrid_to_record_ids",
1858         api_name  => $_,
1859         signature => {
1860             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1861                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1862             params => [
1863                 { desc => 'Metarecord ID', type => 'number' },
1864                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1865             ],
1866             return => {
1867                 desc => 'Results object like {count => $i, ids =>[...]}',
1868                 type => 'object'
1869             }
1870             
1871         }
1872     );
1873 }
1874
1875 sub biblio_mrid_to_record_ids {
1876     my( $self, $client, $mrid, $args ) = @_;
1877
1878     my $format = $$args{format};
1879     my $org    = $$args{org};
1880     my $depth  = $$args{depth};
1881
1882     my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1883     $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1884     my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1885
1886     return { count => scalar(@$recs), ids => $recs };
1887 }
1888
1889
1890 __PACKAGE__->register_method(
1891     method   => "biblio_record_to_marc_html",
1892     api_name => "open-ils.search.biblio.record.html"
1893 );
1894
1895 __PACKAGE__->register_method(
1896     method   => "biblio_record_to_marc_html",
1897     api_name => "open-ils.search.authority.to_html"
1898 );
1899
1900 # Persistent parsers and setting objects
1901 my $parser = XML::LibXML->new();
1902 my $xslt   = XML::LibXSLT->new();
1903 my $marc_sheet;
1904 my $slim_marc_sheet;
1905 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1906
1907 sub biblio_record_to_marc_html {
1908     my($self, $client, $recordid, $slim, $marcxml) = @_;
1909
1910     my $sheet;
1911     my $dir = $settings_client->config_value("dirs", "xsl");
1912
1913     if($slim) {
1914         unless($slim_marc_sheet) {
1915             my $xsl = $settings_client->config_value(
1916                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1917             if($xsl) {
1918                 $xsl = $parser->parse_file("$dir/$xsl");
1919                 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1920             }
1921         }
1922         $sheet = $slim_marc_sheet;
1923     }
1924
1925     unless($sheet) {
1926         unless($marc_sheet) {
1927             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1928             my $xsl = $settings_client->config_value(
1929                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1930             $xsl = $parser->parse_file("$dir/$xsl");
1931             $marc_sheet = $xslt->parse_stylesheet($xsl);
1932         }
1933         $sheet = $marc_sheet;
1934     }
1935
1936     my $record;
1937     unless($marcxml) {
1938         my $e = new_editor();
1939         if($self->api_name =~ /authority/) {
1940             $record = $e->retrieve_authority_record_entry($recordid)
1941                 or return $e->event;
1942         } else {
1943             $record = $e->retrieve_biblio_record_entry($recordid)
1944                 or return $e->event;
1945         }
1946         $marcxml = $record->marc;
1947     }
1948
1949     my $xmldoc = $parser->parse_string($marcxml);
1950     my $html = $sheet->transform($xmldoc);
1951     return $html->documentElement->toString();
1952 }
1953
1954 __PACKAGE__->register_method(
1955     method    => "send_event_email_output",
1956     api_name  => "open-ils.search.biblio.record.email.send_output",
1957 );
1958 sub send_event_email_output {
1959     my($self, $client, $auth, $event_id, $capkey, $capanswer) = @_;
1960     return undef unless $event_id;
1961
1962     my $captcha_pass = 0;
1963     my $real_answer;
1964     if ($capkey) {
1965         $real_answer = $cache->get_cache(md5_hex($capkey));
1966         $captcha_pass++ if ($real_answer eq $capanswer);
1967     }
1968
1969     my $e = new_editor(authtoken => $auth);
1970     return $e->die_event unless $captcha_pass || $e->checkauth;
1971
1972     my $event = $e->retrieve_action_trigger_event([$event_id,{flesh => 1, flesh_fields => { atev => ['template_output']}}]);
1973     return undef unless ($event and $event->template_output);
1974
1975     my $smtp = OpenSRF::Utils::SettingsClient
1976         ->new
1977         ->config_value('email_notify', 'smtp_server');
1978
1979     my $sender = Email::Send->new({mailer => 'SMTP'});
1980     $sender->mailer_args([Host => $smtp]);
1981
1982     my $stat;
1983     my $err;
1984
1985     my $email = _create_mime_email($event->template_output->data);
1986
1987     try {
1988         $stat = $sender->send($email);
1989     } catch Error with {
1990         $err = $stat = shift;
1991         $logger->error("send_event_email_output: Email failed with error: $err");
1992     };
1993
1994     if( !$err and $stat and $stat->type eq 'success' ) {
1995         $logger->info("send_event_email_output: successfully sent email");
1996         return 1;
1997     } else {
1998         $logger->warn("send_event_email_output: unable to send email: ".Dumper($stat));
1999         return 0;
2000     }
2001 }
2002
2003 sub _create_mime_email {
2004     my $template_output = shift;
2005     my $email = Email::MIME->new($template_output);
2006     for my $hfield (qw/From To Bcc Cc Reply-To Sender/) {
2007         my @headers = $email->header($hfield);
2008         $email->header_str_set($hfield => join(',', @headers)) if ($headers[0]);
2009     }
2010
2011     my @headers = $email->header('Subject');
2012     $email->header_str_set('Subject' => $headers[0]) if ($headers[0]);
2013
2014     $email->header_set('MIME-Version' => '1.0');
2015     $email->header_set('Content-Type' => "text/plain; charset=UTF-8");
2016     $email->header_set('Content-Transfer-Encoding' => '8bit');
2017     return $email;
2018 }
2019
2020 __PACKAGE__->register_method(
2021     method    => "format_biblio_record_entry",
2022     api_name  => "open-ils.search.biblio.record.print.preview",
2023 );
2024
2025 __PACKAGE__->register_method(
2026     method    => "format_biblio_record_entry",
2027     api_name  => "open-ils.search.biblio.record.email.preview",
2028 );
2029
2030 __PACKAGE__->register_method(
2031     method    => "format_biblio_record_entry",
2032     api_name  => "open-ils.search.biblio.record.print",
2033     signature => {
2034         desc   => 'Returns a printable version of the specified bib record',
2035         params => [
2036             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
2037             { desc => 'Context library for holdings, if applicable', type => 'number' },
2038             { desc => 'Sort order, if applicable', type => 'string' },
2039             { desc => 'Sort direction, if applicable', type => 'string' },
2040             { desc => 'Definition Group Member id', type => 'number' },
2041         ],
2042         return => {
2043             desc => q/An action_trigger.event object or error event./,
2044             type => 'object',
2045         }
2046     }
2047 );
2048 __PACKAGE__->register_method(
2049     method    => "format_biblio_record_entry",
2050     api_name  => "open-ils.search.biblio.record.email",
2051     signature => {
2052         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
2053         params => [
2054             { desc => 'Authentication token', type => 'string'},
2055             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
2056             { desc => 'Context library for holdings, if applicable', type => 'number' },
2057             { desc => 'Sort order, if applicable', type => 'string' },
2058             { desc => 'Sort direction, if applicable', type => 'string' },
2059             { desc => 'Definition Group Member id', type => 'number' },
2060             { desc => 'Whether to bypass auth due to captcha', type => 'bool' },
2061             { desc => 'Email address, if none for the user', type => 'string' },
2062             { desc => 'Subject, if customized', type => 'string' },
2063         ],
2064         return => {
2065             desc => q/Undefined on success, otherwise an error event./,
2066             type => 'object',
2067         }
2068     }
2069 );
2070
2071 sub format_biblio_record_entry {
2072     my ($self, $conn) = splice @_, 0, 2;
2073
2074     my $for_print = ($self->api_name =~ /print/);
2075     my $for_email = ($self->api_name =~ /email/);
2076     my $preview = ($self->api_name =~ /preview/);
2077
2078     my ($auth, $captcha_pass, $email, $subject);
2079     if ($for_email) {
2080         $auth = shift @_;
2081         if (@_ > 5) { # the stuff below is included in the params, safe to splice
2082             ($captcha_pass, $email, $subject) = splice @_, -3, 3;
2083         }
2084     }
2085     my ($bib_id, $holdings_context_org, $bib_sort, $sort_dir, $group_member) = @_;
2086     $holdings_context_org ||= $U->get_org_tree->id;
2087     $bib_sort ||= 'author';
2088     $sort_dir ||= 'ascending';
2089
2090     my $e; my $event_context_org; my $type = 'brief';
2091
2092     if ($for_print) {
2093         $event_context_org = $holdings_context_org;
2094         $e = new_editor(xact => 1);
2095     } elsif ($for_email) {
2096         $e = new_editor(authtoken => $auth, xact => 1);
2097         return $e->die_event unless $captcha_pass || $e->checkauth;
2098         $event_context_org = $e->requestor ? $e->requestor->home_ou : $holdings_context_org;
2099         $email ||= $e->requestor ? $e->requestor->email : '';
2100     }
2101
2102     if ($group_member) {
2103         $group_member = $e->retrieve_action_trigger_event_def_group_member($group_member);
2104         if ($group_member and $U->is_true($group_member->holdings)) {
2105             $type = 'full';
2106         }
2107     }
2108
2109     $holdings_context_org = $e->retrieve_actor_org_unit($holdings_context_org);
2110
2111     my $bib_ids;
2112     if (ref $bib_id ne 'ARRAY') {
2113         $bib_ids = [ $bib_id ];
2114     } else {
2115         $bib_ids = $bib_id;
2116     }
2117
2118     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
2119     $bucket->btype('temp');
2120     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
2121     if ($for_email) {
2122         $bucket->owner($e->requestor || 1) 
2123     } else {
2124         $bucket->owner(1);
2125     }
2126     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
2127
2128     for my $id (@$bib_ids) {
2129
2130         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
2131
2132         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
2133         $bucket_entry->target_biblio_record_entry($bib);
2134         $bucket_entry->bucket($bucket_obj->id);
2135         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
2136     }
2137
2138     $e->commit;
2139
2140     my $usr_data = {
2141         type        => $type,
2142         email       => $email,
2143         subject     => $subject,
2144         context_org => $holdings_context_org->shortname,
2145         sort_by     => $bib_sort,
2146         sort_dir    => $sort_dir,
2147         preview     => $preview
2148     };
2149
2150     if ($for_print) {
2151
2152         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $event_context_org, undef, [ $usr_data ]);
2153
2154     } elsif ($for_email) {
2155
2156         return $U->fire_object_event(undef, 'biblio.format.record_entry.email', [ $bucket ], $event_context_org, undef, [ $usr_data ])
2157             if ($preview);
2158
2159         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $event_context_org, undef, $usr_data, 1);
2160     }
2161
2162     return undef;
2163 }
2164
2165
2166 __PACKAGE__->register_method(
2167     method   => "retrieve_all_copy_statuses",
2168     api_name => "open-ils.search.config.copy_status.retrieve.all"
2169 );
2170
2171 sub retrieve_all_copy_statuses {
2172     my( $self, $client ) = @_;
2173     return new_editor()->retrieve_all_config_copy_status();
2174 }
2175
2176
2177 __PACKAGE__->register_method(
2178     method   => "copy_counts_per_org",
2179     api_name => "open-ils.search.biblio.copy_counts.retrieve"
2180 );
2181
2182 __PACKAGE__->register_method(
2183     method   => "copy_counts_per_org",
2184     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
2185 );
2186
2187 sub copy_counts_per_org {
2188     my( $self, $client, $record_id ) = @_;
2189
2190     warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
2191
2192     my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
2193     if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
2194
2195     my $counts = $apputils->simple_scalar_request(
2196         "open-ils.storage", $method, $record_id );
2197
2198     $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
2199     return $counts;
2200 }
2201
2202
2203 __PACKAGE__->register_method(
2204     method   => "copy_count_summary",
2205     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
2206     notes    => "returns an array of these: "
2207               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2208               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2209 );
2210         
2211
2212 sub copy_count_summary {
2213     my( $self, $client, $rid, $org, $depth ) = @_;
2214     $org   ||= 1;
2215     $depth ||= 0;
2216     my $data = $U->storagereq(
2217         'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
2218
2219     return [ sort {
2220         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2221         cmp
2222         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2223     } @$data ];
2224 }
2225
2226 __PACKAGE__->register_method(
2227     method   => "copy_location_count_summary",
2228     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
2229     notes    => "returns an array of these: "
2230               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
2231               . "where statusx is a copy status name.  The statuses are sorted by ID.",
2232 );
2233
2234 sub copy_location_count_summary {
2235     my( $self, $client, $rid, $org, $depth ) = @_;
2236     $org   ||= 1;
2237     $depth ||= 0;
2238     my $data = $U->storagereq(
2239         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2240
2241     return [ sort {
2242         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2243         cmp
2244         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2245
2246         || $a->[4] cmp $b->[4]
2247     } @$data ];
2248 }
2249
2250 __PACKAGE__->register_method(
2251     method   => "copy_count_location_summary",
2252     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2253     notes    => "returns an array of these: "
2254               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2255               . "where statusx is a copy status name.  The statuses are sorted by ID."
2256 );
2257
2258 sub copy_count_location_summary {
2259     my( $self, $client, $rid, $org, $depth ) = @_;
2260     $org   ||= 1;
2261     $depth ||= 0;
2262     my $data = $U->storagereq(
2263         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2264     return [ sort {
2265         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2266         cmp
2267         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2268     } @$data ];
2269 }
2270
2271
2272 foreach (qw/open-ils.search.biblio.marc
2273             open-ils.search.biblio.marc.staff/)
2274 {
2275 __PACKAGE__->register_method(
2276     method    => "marc_search",
2277     api_name  => $_,
2278     signature => {
2279         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2280                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2281         params => [
2282             {
2283                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2284                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2285                 type => 'object'
2286             },
2287             {desc => 'timeout (optional)',  type => 'number'}
2288         ],
2289         return => {
2290             desc => 'Results object like: { "count": $i, "ids": [...] }',
2291             type => 'object'
2292         }
2293     }
2294 );
2295 }
2296
2297 =head3 open-ils.search.biblio.marc (arghash, timeout)
2298
2299 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2300
2301     searches: complex query object  (required)
2302     org_unit: The org ID to focus the search at
2303     depth   : The org depth     
2304     limit   : integer search limit      default: 10
2305     offset  : integer search offset     default:  0
2306     sort    : What field to sort the results on? [ author | title | pubdate ]
2307     sort_dir: In what direction do we sort? [ asc | desc ]
2308
2309 Additional keys to refine search criteria:
2310
2311     audience : Audience
2312     language : Language (code)
2313     lit_form : Literary form
2314     item_form: Item form
2315     item_type: Item type
2316     format   : The MARC format
2317
2318 Please note that the specific strings to be used in the "addtional keys" will be entirely
2319 dependent on your loaded data.  
2320
2321 All keys except "searches" are optional.
2322 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2323
2324 For example, an arg hash might look like:
2325
2326     $arghash = {
2327         searches => [
2328             {
2329                 term     => "harry",
2330                 restrict => [
2331                     {
2332                         tag => 245,
2333                         subfield => "a"
2334                     }
2335                     # ...
2336                 ]
2337             }
2338             # ...
2339         ],
2340         org_unit  => 1,
2341         limit     => 5,
2342         sort      => "author",
2343         item_type => "g"
2344     }
2345
2346 The arghash is eventually passed to the SRF call:
2347 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2348
2349 Presently, search uses the cache unconditionally.
2350
2351 =cut
2352
2353 # FIXME: that example above isn't actually tested.
2354 # FIXME: sort and limit added.  item_type not tested yet.
2355 # TODO: docache option?
2356 sub marc_search {
2357     my( $self, $conn, $args, $timeout ) = @_;
2358
2359     my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2360     $method .= ".staff" if $self->api_name =~ /staff/;
2361     $method .= ".atomic";
2362
2363     my $limit = $args->{limit} || 10;
2364     my $offset = $args->{offset} || 0;
2365
2366     # allow caller to pass in a call timeout since MARC searches
2367     # can take longer than the default 60-second timeout.  
2368     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2369     $timeout = 120 if !$timeout or $timeout > 300;
2370
2371     my @search;
2372     push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2373     my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2374
2375     my $recs = search_cache($ckey, $offset, $limit);
2376
2377     if(!$recs) {
2378
2379         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2380         my $req = $ses->request($method, %$args);
2381         my $resp = $req->recv($timeout);
2382
2383         if($resp and $recs = $resp->content) {
2384             put_cache($ckey, scalar(@$recs), $recs);
2385         } else {
2386             $recs = [];
2387         }
2388
2389         $ses->kill_me;
2390     }
2391
2392     my $count = 0;
2393     $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2394     my @recs = map { $_->[0] } @$recs;
2395
2396     return { ids => \@recs, count => $count };
2397 }
2398
2399
2400 foreach my $isbn_method (qw/
2401     open-ils.search.biblio.isbn
2402     open-ils.search.biblio.isbn.staff
2403 /) {
2404 __PACKAGE__->register_method(
2405     method    => "biblio_search_isbn",
2406     api_name  => $isbn_method,
2407     signature => {
2408         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2409         params => [
2410             {desc => 'ISBN', type => 'string'}
2411         ],
2412         return => {
2413             desc => 'Results object like: { "count": $i, "ids": [...] }',
2414             type => 'object'
2415         }
2416     }
2417 );
2418 }
2419
2420 sub biblio_search_isbn { 
2421     my( $self, $client, $isbn ) = @_;
2422     $logger->debug("Searching ISBN $isbn");
2423     # the previous implementation of this method was essentially unlimited,
2424     # so we will set our limit very high and let multiclass.query provide any
2425     # actual limit
2426     # XXX: if making this unlimited is deemed important, we might consider
2427     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2428     # which is functionally deprecated at this point, or a custom call to
2429     # 'open-ils.storage.biblio.multiclass.search_fts'
2430
2431     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2432     if ($self->api_name =~ m/.staff$/) {
2433         $isbn_method .= '.staff';
2434     }
2435
2436     my $method = $self->method_lookup($isbn_method);
2437     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2438     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2439     return { ids => \@recs, count => $search_result->{'count'} };
2440 }
2441
2442 __PACKAGE__->register_method(
2443     method   => "biblio_search_isbn_batch",
2444     api_name => "open-ils.search.biblio.isbn_list",
2445 );
2446
2447 # XXX: see biblio_search_isbn() for note concerning 'limit'
2448 sub biblio_search_isbn_batch { 
2449     my( $self, $client, $isbn_list ) = @_;
2450     $logger->debug("Searching ISBNs @$isbn_list");
2451     my @recs = (); my %rec_set = ();
2452     my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2453     foreach my $isbn ( @$isbn_list ) {
2454         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2455         my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2456         foreach my $rec (@recs_subset) {
2457             if (! $rec_set{ $rec }) {
2458                 $rec_set{ $rec } = 1;
2459                 push @recs, $rec;
2460             }
2461         }
2462     }
2463     return { ids => \@recs, count => int(scalar(@recs)) };
2464 }
2465
2466 foreach my $issn_method (qw/
2467     open-ils.search.biblio.issn
2468     open-ils.search.biblio.issn.staff
2469 /) {
2470 __PACKAGE__->register_method(
2471     method   => "biblio_search_issn",
2472     api_name => $issn_method,
2473     signature => {
2474         desc   => 'Retrieve biblio IDs for a given ISSN',
2475         params => [
2476             {desc => 'ISBN', type => 'string'}
2477         ],
2478         return => {
2479             desc => 'Results object like: { "count": $i, "ids": [...] }',
2480             type => 'object'
2481         }
2482     }
2483 );
2484 }
2485
2486 sub biblio_search_issn { 
2487     my( $self, $client, $issn ) = @_;
2488     $logger->debug("Searching ISSN $issn");
2489     # the previous implementation of this method was essentially unlimited,
2490     # so we will set our limit very high and let multiclass.query provide any
2491     # actual limit
2492     # XXX: if making this unlimited is deemed important, we might consider
2493     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2494     # which is functionally deprecated at this point, or a custom call to
2495     # 'open-ils.storage.biblio.multiclass.search_fts'
2496
2497     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2498     if ($self->api_name =~ m/.staff$/) {
2499         $issn_method .= '.staff';
2500     }
2501
2502     my $method = $self->method_lookup($issn_method);
2503     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2504     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2505     return { ids => \@recs, count => $search_result->{'count'} };
2506 }
2507
2508
2509 __PACKAGE__->register_method(
2510     method    => "fetch_mods_by_copy",
2511     api_name  => "open-ils.search.biblio.mods_from_copy",
2512     argc      => 1,
2513     signature => {
2514         desc    => 'Retrieve MODS record given an attached copy ID',
2515         params  => [
2516             { desc => 'Copy ID', type => 'number' }
2517         ],
2518         returns => {
2519             desc => 'MODS record, event on error or uncataloged item'
2520         }
2521     }
2522 );
2523
2524 sub fetch_mods_by_copy {
2525     my( $self, $client, $copyid ) = @_;
2526     my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2527     return $evt if $evt;
2528     return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2529     return $apputils->record_to_mvr($record);
2530 }
2531
2532
2533 # -------------------------------------------------------------------------------------
2534
2535 __PACKAGE__->register_method(
2536     method   => "cn_browse",
2537     api_name => "open-ils.search.callnumber.browse.target",
2538     notes    => "Starts a callnumber browse"
2539 );
2540
2541 __PACKAGE__->register_method(
2542     method   => "cn_browse",
2543     api_name => "open-ils.search.callnumber.browse.page_up",
2544     notes    => "Returns the previous page of callnumbers",
2545 );
2546
2547 __PACKAGE__->register_method(
2548     method   => "cn_browse",
2549     api_name => "open-ils.search.callnumber.browse.page_down",
2550     notes    => "Returns the next page of callnumbers",
2551 );
2552
2553
2554 # RETURNS array of arrays like so: label, owning_lib, record, id
2555 sub cn_browse {
2556     my( $self, $client, @params ) = @_;
2557     my $method;
2558
2559     $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2560         if( $self->api_name =~ /target/ );
2561     $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2562         if( $self->api_name =~ /page_up/ );
2563     $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2564         if( $self->api_name =~ /page_down/ );
2565
2566     return $apputils->simplereq( 'open-ils.storage', $method, @params );
2567 }
2568 # -------------------------------------------------------------------------------------
2569
2570 __PACKAGE__->register_method(
2571     method        => "fetch_cn",
2572     api_name      => "open-ils.search.callnumber.retrieve",
2573     authoritative => 1,
2574     notes         => "retrieves a callnumber based on ID",
2575 );
2576
2577 sub fetch_cn {
2578     my( $self, $client, $id ) = @_;
2579
2580     my $e = new_editor();
2581     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2582     return $evt if $evt;
2583     return $cn;
2584 }
2585
2586 __PACKAGE__->register_method(
2587     method        => "fetch_fleshed_cn",
2588     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2589     authoritative => 1,
2590     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2591 );
2592
2593 sub fetch_fleshed_cn {
2594     my( $self, $client, $id ) = @_;
2595
2596     my $e = new_editor();
2597     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2598     return $evt if $evt;
2599     return $cn;
2600 }
2601
2602
2603 __PACKAGE__->register_method(
2604     method    => "fetch_copy_by_cn",
2605     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2606     signature => q/
2607         Returns an array of copy ID's by callnumber ID
2608         @param cnid The callnumber ID
2609         @return An array of copy IDs
2610     /
2611 );
2612
2613 sub fetch_copy_by_cn {
2614     my( $self, $conn, $cnid ) = @_;
2615     return $U->cstorereq(
2616         'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2617         { call_number => $cnid, deleted => 'f' } );
2618 }
2619
2620 __PACKAGE__->register_method(
2621     method    => 'fetch_cn_by_info',
2622     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2623     signature => q/
2624         @param label The callnumber label
2625         @param record The record the cn is attached to
2626         @param org The owning library of the cn
2627         @return The callnumber object
2628     /
2629 );
2630
2631
2632 sub fetch_cn_by_info {
2633     my( $self, $conn, $label, $record, $org ) = @_;
2634     return $U->cstorereq(
2635         'open-ils.cstore.direct.asset.call_number.search',
2636         { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2637 }
2638
2639
2640
2641 __PACKAGE__->register_method(
2642     method   => 'bib_extras',
2643     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2644     ctype => 'lit_form'
2645 );
2646 __PACKAGE__->register_method(
2647     method   => 'bib_extras',
2648     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2649     ctype => 'item_form'
2650 );
2651 __PACKAGE__->register_method(
2652     method   => 'bib_extras',
2653     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2654     ctype => 'item_type',
2655 );
2656 __PACKAGE__->register_method(
2657     method   => 'bib_extras',
2658     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2659     ctype => 'bib_level'
2660 );
2661 __PACKAGE__->register_method(
2662     method   => 'bib_extras',
2663     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2664     ctype => 'audience'
2665 );
2666
2667 sub bib_extras {
2668     my $self = shift;
2669     $logger->warn("deprecation warning: " .$self->api_name);
2670
2671     my $e = new_editor();
2672
2673     my $ctype = $self->{ctype};
2674     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2675
2676     my @objs;
2677     for my $ccvm (@$ccvms) {
2678         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2679         $obj->value($ccvm->value);
2680         $obj->code($ccvm->code);
2681         $obj->description($ccvm->description) if $obj->can('description');
2682         push(@objs, $obj);
2683     }
2684
2685     return \@objs;
2686 }
2687
2688
2689
2690 __PACKAGE__->register_method(
2691     method    => 'fetch_slim_record',
2692     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2693     signature => {
2694         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2695         params => [
2696             { desc => 'Array of Record IDs', type => 'array' }
2697         ],
2698         return => { 
2699             desc => 'Array of biblio records, event on error'
2700         }
2701     }
2702 );
2703
2704 sub fetch_slim_record {
2705     my( $self, $conn, $ids ) = @_;
2706
2707     my $editor = new_editor();
2708     my @res;
2709     for( @$ids ) {
2710         return $editor->event unless
2711             my $r = $editor->retrieve_biblio_record_entry($_);
2712         $r->clear_marc;
2713         push(@res, $r);
2714     }
2715     return \@res;
2716 }
2717
2718 __PACKAGE__->register_method(
2719     method    => 'rec_hold_parts',
2720     api_name  => 'open-ils.search.biblio.record_hold_parts',
2721     signature => q/
2722        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2723     /
2724 );
2725
2726 sub rec_hold_parts {
2727     my( $self, $conn, $args ) = @_;
2728
2729     my $rec        = $$args{record};
2730     my $mrec       = $$args{metarecord};
2731     my $pickup_lib = $$args{pickup_lib};
2732     my $e = new_editor();
2733
2734     my $query = {
2735         select => {bmp => ['id', 'label']},
2736         from => 'bmp',
2737         where => {
2738             id => {
2739                 in => {
2740                     select => {'acpm' => ['part']},
2741                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2742                     where => {
2743                         '+acp' => {'deleted' => 'f'},
2744                         '+bre' => {id => $rec}
2745                     },
2746                     distinct => 1,
2747                 }
2748             },
2749             deleted => 'f'
2750         },
2751         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2752     };
2753
2754     if(defined $pickup_lib) {
2755         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2756         if($hard_boundary) {
2757             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2758             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2759         }
2760     }
2761
2762     return $e->json_query($query);
2763 }
2764
2765
2766
2767
2768 __PACKAGE__->register_method(
2769     method    => 'rec_to_mr_rec_descriptors',
2770     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2771     signature => q/
2772         specialized method...
2773         Given a biblio record id or a metarecord id, 
2774         this returns a list of metabib.record_descriptor
2775         objects that live within the same metarecord
2776         @param args Object of args including:
2777     /
2778 );
2779
2780 sub rec_to_mr_rec_descriptors {
2781     my( $self, $conn, $args ) = @_;
2782
2783     my $rec        = $$args{record};
2784     my $mrec       = $$args{metarecord};
2785     my $item_forms = $$args{item_forms};
2786     my $item_types = $$args{item_types};
2787     my $item_lang  = $$args{item_lang};
2788     my $pickup_lib = $$args{pickup_lib};
2789
2790     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2791
2792     my $e = new_editor();
2793     my $recs;
2794
2795     if( !$mrec ) {
2796         my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2797         return $e->event unless @$map;
2798         $mrec = $$map[0]->metarecord;
2799     }
2800
2801     $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2802     return $e->event unless @$recs;
2803
2804     my @recs = map { $_->source } @$recs;
2805     my $search = { record => \@recs };
2806     $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2807     $search->{item_type} = $item_types if $item_types and @$item_types;
2808     $search->{item_lang} = $item_lang  if $item_lang;
2809
2810     my $desc = $e->search_metabib_record_descriptor($search);
2811
2812     my $query = {
2813         distinct => 1,
2814         select   => { 'bre' => ['id'] },
2815         from     => {
2816             'bre' => {
2817                 'acn' => {
2818                     'join' => {
2819                         'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2820                       }
2821                   }
2822              }
2823         },
2824         where => {
2825             '+bre' => { id => \@recs },
2826             '+acp' => {
2827                 holdable => 't',
2828                 deleted  => 'f'
2829             },
2830             "+ccs" => { holdable => 't' },
2831             "+acpl" => { holdable => 't', deleted => 'f' }
2832         }
2833     };
2834
2835     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2836         my $orgs = $e->json_query(
2837             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2838         ) or return $e->die_event;
2839
2840         $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2841     }
2842
2843     my $good_records = $e->json_query($query) or return $e->die_event;
2844
2845     my @keep;
2846     for my $d (@$desc) {
2847         if ( grep { $d->record == $_->{id} } @$good_records ) {
2848             push @keep, $d;
2849         }
2850     }
2851
2852     $desc = \@keep;
2853
2854     return { metarecord => $mrec, descriptors => $desc };
2855 }
2856
2857
2858 __PACKAGE__->register_method(
2859     method   => 'fetch_age_protect',
2860     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2861 );
2862
2863 sub fetch_age_protect {
2864     return new_editor()->retrieve_all_config_rule_age_hold_protect();
2865 }
2866
2867
2868 __PACKAGE__->register_method(
2869     method   => 'copies_by_cn_label',
2870     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2871 );
2872
2873 __PACKAGE__->register_method(
2874     method   => 'copies_by_cn_label',
2875     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2876 );
2877
2878 sub copies_by_cn_label {
2879     my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2880     my $e = new_editor();
2881     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2882     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2883     my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2884     return [] unless @$cns;
2885
2886     # show all non-deleted copies in the staff client ...
2887     if ($self->api_name =~ /staff$/o) {
2888         return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2889     }
2890
2891     # ... otherwise, grab the copies ...
2892     my $copies = $e->search_asset_copy(
2893         [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2894           {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2895         ]
2896     );
2897
2898     # ... and test for location and status visibility
2899     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2900 }
2901
2902 __PACKAGE__->register_method(
2903     method   => 'bib_copies',
2904     api_name => 'open-ils.search.bib.copies',
2905     stream => 1
2906 );
2907 __PACKAGE__->register_method(
2908     method   => 'bib_copies',
2909     api_name => 'open-ils.search.bib.copies.staff',
2910     stream => 1
2911 );
2912
2913 sub bib_copies {
2914     my ($self, $client, $rec_id, $org, $depth, $limit, $offset, $pref_ou) = @_;
2915     my $is_staff = ($self->api_name =~ /staff/);
2916
2917     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
2918     my $req = $cstore->request(
2919         'open-ils.cstore.json_query', mk_copy_query(
2920         $rec_id, $org, $depth, $limit, $offset, $pref_ou, $is_staff));
2921
2922     my $resp;
2923     while ($resp = $req->recv) {
2924         my $copy = $resp->content;
2925
2926         if ($is_staff) {
2927             # last_circ is an IDL query so it cannot be queried directly
2928             # via JSON query.
2929             $copy->{last_circ} = 
2930                 new_editor()->retrieve_reporter_last_circ_date($copy->{id})
2931                 ->last_circ;
2932         }
2933
2934         $client->respond($copy);
2935     }
2936
2937     return undef;
2938 }
2939
2940 # TODO: this comes almost directly from WWW/EGCatLoader/Record.pm
2941 # Refactor to share
2942 sub mk_copy_query {
2943     my $rec_id = shift;
2944     my $org = shift;
2945     my $depth = shift;
2946     my $copy_limit = shift;
2947     my $copy_offset = shift;
2948     my $pref_ou = shift;
2949     my $is_staff = shift;
2950     my $base_query = shift;
2951
2952     my $query = $base_query || $U->basic_opac_copy_query(
2953         $rec_id, undef, undef, $copy_limit, $copy_offset, $is_staff
2954     );
2955
2956     if ($org) { # TODO: root org test
2957         # no need to add the org join filter if we're not actually filtering
2958         $query->{from}->{acp}->[1] = { aou => {
2959             fkey => 'circ_lib',
2960             field => 'id',
2961             filter => {
2962                 id => {
2963                     in => {
2964                         select => {aou => [{
2965                             column => 'id', 
2966                             transform => 'actor.org_unit_descendants',
2967                             result_field => 'id', 
2968                             params => [$depth]
2969                         }]},
2970                         from => 'aou',
2971                         where => {id => $org}
2972                     }
2973                 }
2974             }
2975         }};
2976
2977         if ($pref_ou) {
2978             # Make sure the pref OU is included in the results
2979             my $in = $query->{from}->{acp}->[1]->{aou}->{filter}->{id}->{in};
2980             delete $query->{from}->{acp}->[1]->{aou}->{filter}->{id};
2981             $query->{from}->{acp}->[1]->{aou}->{filter}->{'-or'} = [
2982                 {id => {in => $in}},
2983                 {id => $pref_ou}
2984             ];
2985         }
2986     };
2987
2988     # Unsure if we want these in the shared function, leaving here for now
2989     unshift(@{$query->{order_by}},
2990         { class => "aou", field => 'id',
2991           transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
2992         }
2993     );
2994     push(@{$query->{order_by}},
2995         { class => "acp", field => 'id',
2996           transform => 'evergreen.rank_cp'
2997         }
2998     );
2999
3000     return $query;
3001 }
3002
3003 __PACKAGE__->register_method(
3004     method    => 'record_urls',
3005     api_name  => 'open-ils.search.biblio.record.resource_urls.retrieve',
3006     argc      => 1,
3007     stream    => 1,
3008     signature => {
3009         desc   => q/Returns bib record 856 URL content./,
3010         params => [
3011             {desc => 'Context org unit ID', type => 'number'},
3012             {desc => 'Record ID or Array of Record IDs', type => 'number or array'}
3013         ],
3014         return => {
3015             desc => 'Stream of URL objects, one collection object per record',
3016             type => 'object'
3017         }
3018     }
3019 );
3020
3021 sub record_urls {
3022     my ($self, $client, $org_id, $record_ids) = @_;
3023
3024     $record_ids = [$record_ids] unless ref $record_ids eq 'ARRAY';
3025
3026     my $e = new_editor();
3027
3028     for my $record_id (@$record_ids) {
3029
3030         my @urls;
3031
3032         # Start with scoped located URIs
3033         my $uris = $e->json_query({
3034             from => ['evergreen.located_uris_as_uris', $record_id, $org_id]});
3035
3036         for my $uri (@$uris) {
3037             push(@urls, {
3038                 href => $uri->{href},
3039                 label => $uri->{label},
3040                 note => $uri->{use_restriction}
3041             });
3042         }
3043
3044         # Logic copied from TPAC misc_utils.tts
3045         my $bib = $e->retrieve_biblio_record_entry($record_id)
3046             or return $e->event;
3047
3048         my $marc_doc = $U->marc_xml_to_doc($bib->marc);
3049
3050         for my $node ($marc_doc->findnodes('//*[@tag="856" and @ind1="4"]')) {
3051
3052             # asset.uri's
3053             next if $node->findnodes('./*[@code="9" or @code="w" or @code="n"]');
3054
3055             my $url = {};
3056             my ($label) = $node->findnodes('./*[@code="y"]');
3057             my ($notes) = $node->findnodes('./*[@code="z" or @code="3"]');
3058
3059             my $first = 1;
3060             for my $href_node ($node->findnodes('./*[@code="u"]')) {
3061                 next unless $href_node;
3062
3063                 # it's possible for multiple $u's to exist within 1 856 tag.
3064                 # in that case, honor the label/notes data for the first $u, but
3065                 # leave any subsequent $u's as unadorned href's.
3066                 # use href/link/note keys to be consistent with args.uri's
3067
3068                 my $href = $href_node->textContent;
3069                 push(@urls, {
3070                     href => $href,
3071                     label => ($first && $label) ?  $label->textContent : $href,
3072                     note => ($first && $notes) ? $notes->textContent : '',
3073                     ind2 => $node->getAttribute('ind2')
3074                 });
3075                 $first = 0;
3076             }
3077         }
3078
3079         $client->respond({id => $record_id, urls => \@urls});
3080     }
3081
3082     return undef;
3083 }
3084
3085 __PACKAGE__->register_method(
3086     method    => 'catalog_record_summary',
3087     api_name  => 'open-ils.search.biblio.record.catalog_summary',
3088     stream    => 1,
3089     max_bundle_count => 1,
3090     signature => {
3091         desc   => 'Stream of record data suitable for catalog display',
3092         params => [
3093             {desc => 'Context org unit ID', type => 'number'},
3094             {desc => 'Array of Record IDs', type => 'array'}
3095         ],
3096         return => { 
3097             desc => q/
3098                 Stream of record summary objects including id, record,
3099                 hold_count, copy_counts, display (metabib display
3100                 fields), and attributes (metabib record attrs).  The
3101                 metabib variant of the call gets metabib_id and
3102                 metabib_records, and the regular record version also
3103                 gets some metabib information, but returns them as
3104                 staff_view_metabib_id, staff_view_metabib_records, and
3105                 staff_view_metabib_attributes.  This is to mitigate the
3106                 need for code changes elsewhere where assumptions are
3107                 made when certain fields are returned.
3108                 
3109             /
3110         }
3111     }
3112 );
3113 __PACKAGE__->register_method(
3114     method    => 'catalog_record_summary',
3115     api_name  => 'open-ils.search.biblio.record.catalog_summary.staff',
3116     stream    => 1,
3117     max_bundle_count => 1,
3118     signature => q/see open-ils.search.biblio.record.catalog_summary/
3119 );
3120 __PACKAGE__->register_method(
3121     method    => 'catalog_record_summary',
3122     api_name  => 'open-ils.search.biblio.metabib.catalog_summary',
3123     stream    => 1,
3124     max_bundle_count => 1,
3125     signature => q/see open-ils.search.biblio.record.catalog_summary/
3126 );
3127
3128 __PACKAGE__->register_method(
3129     method    => 'catalog_record_summary',
3130     api_name  => 'open-ils.search.biblio.metabib.catalog_summary.staff',
3131     stream    => 1,
3132     max_bundle_count => 1,
3133     signature => q/see open-ils.search.biblio.record.catalog_summary/
3134 );
3135
3136
3137 sub catalog_record_summary {
3138     my ($self, $client, $org_id, $record_ids, $options) = @_;
3139     my $e = new_editor();
3140     $options ||= {};
3141     my $pref_ou = $options->{pref_ou};
3142
3143     my $is_meta = ($self->api_name =~ /metabib/);
3144     my $is_staff = ($self->api_name =~ /staff/);
3145
3146     my $holds_method = $is_meta ? 
3147         'open-ils.circ.mmr.holds.count' : 
3148         'open-ils.circ.bre.holds.count';
3149
3150     my $copy_method = $is_meta ? 
3151         'open-ils.search.biblio.metarecord.copy_count':
3152         'open-ils.search.biblio.record.copy_count';
3153
3154     $copy_method .= '.staff' if $is_staff;
3155
3156     $copy_method = $self->method_lookup($copy_method); # local method
3157
3158     my $holdable_method = $is_meta ?
3159         'open-ils.search.biblio.metarecord.has_holdable_copy':
3160         'open-ils.search.biblio.record.has_holdable_copy';
3161
3162     $holdable_method = $self->method_lookup($holdable_method); # local method
3163
3164     my %MR_summary_cache;
3165     for my $rec_id (@$record_ids) {
3166
3167         my $response = $is_meta ? 
3168             get_one_metarecord_summary($self, $e, $org_id, $rec_id) :
3169             get_one_record_summary($self, $e, $org_id, $rec_id);
3170
3171         # Let's get Formats & Editions data FIXME: consider peer bibs?
3172         my @metabib_records;
3173         unless ($is_meta) {
3174             my $meta_search = $e->search_metabib_metarecord_source_map({source => $rec_id});
3175             if (scalar(@$meta_search) > 0) {
3176                 $response->{staff_view_metabib_id} = $meta_search->[0]->metarecord;
3177                 my $maps = $e->search_metabib_metarecord_source_map({metarecord => $response->{staff_view_metabib_id}});
3178                 @metabib_records = map { $_->source } @$maps;
3179             } else {
3180                 # XXX ugly hack for bibs without metarecord mappings, e.g. deleted bibs
3181                 # where ingest.metarecord_mapping.preserve_on_delete is false
3182                 @metabib_records = ( $rec_id );
3183             }
3184
3185             $response->{staff_view_metabib_records} = \@metabib_records;
3186
3187             my $metabib_attr = {};
3188             my $attributes;
3189             if ($response->{staff_view_metabib_id} and $MR_summary_cache{$response->{staff_view_metabib_id}}) {
3190                 $metabib_attr = $MR_summary_cache{$response->{staff_view_metabib_id}};
3191             } else {
3192                 $attributes = $U->get_bre_attrs(\@metabib_records);
3193             }
3194
3195             # we get "243":{
3196             #       "srce":{
3197             #         "code":" ",
3198             #         "label":"National bibliographic agency"
3199             #       }, ...}
3200
3201             if ($attributes) {
3202                 foreach my $bib_id ( keys %{ $attributes } ) {
3203                     foreach my $ctype ( keys %{ $attributes->{$bib_id} } ) {
3204                         # we want {
3205                         #   "srce":{ " ": { "label": "National bibliographic agency", "count" : 1 } },
3206                         #       ...
3207                         #   }
3208                         my $current_code = $attributes->{$bib_id}->{$ctype}->{code};
3209                         my $code_label = $attributes->{$bib_id}->{$ctype}->{label};
3210                         $metabib_attr->{$ctype} = {} unless $metabib_attr->{$ctype};
3211                         if (! $metabib_attr->{$ctype}->{ $current_code }) {
3212                             $metabib_attr->{$ctype}->{ $current_code } = {
3213                                 "label" => $code_label,
3214                                 "count" => 1
3215                             }
3216                         } else {
3217                             $metabib_attr->{$ctype}->{ $current_code }->{count}++;
3218                         }
3219                     }
3220                 }
3221             }
3222
3223             if ($response->{staff_view_metabib_id}) {
3224                 $MR_summary_cache{$response->{staff_view_metabib_id}} = $metabib_attr;
3225             }
3226             $response->{staff_view_metabib_attributes} = $metabib_attr;
3227         }
3228
3229         ($response->{copy_counts}) = $copy_method->run($org_id, $rec_id);
3230
3231         $response->{first_call_number} = get_first_call_number(
3232             $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3233
3234         if ($pref_ou) {
3235
3236             # If we already have the pref ou copy counts, avoid the extra fetch.
3237             my ($match) = 
3238                 grep {$_->{org_unit} eq $pref_ou} @{$response->{copy_counts}};
3239
3240             if (!$match) {
3241                 my ($counts) = $copy_method->run($pref_ou, $rec_id);
3242                 ($match) = grep {$_->{org_unit} eq $pref_ou} @$counts;
3243             }
3244
3245             $response->{pref_ou_copy_counts} = $match;
3246         }
3247
3248         $response->{hold_count} = 
3249             $U->simplereq('open-ils.circ', $holds_method, $rec_id);
3250
3251         if ($options->{flesh_copies}) {
3252             $response->{copies} = get_representative_copies(
3253                 $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3254         }
3255
3256         ($response->{has_holdable_copy}) = $holdable_method->run($rec_id);
3257
3258         $client->respond($response);
3259     }
3260
3261     return undef;
3262 }
3263
3264 # Returns a snapshot of copy information for a given record or metarecord,
3265 # sorted by pref org and search org.
3266 sub get_representative_copies {
3267     my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
3268
3269     my @rec_ids;
3270     my $limit = $options->{copy_limit};
3271     my $copy_depth = $options->{copy_depth};
3272     my $copy_offset = $options->{copy_offset};
3273     my $pref_ou = $options->{pref_ou};
3274
3275     my $org_tree = $U->get_org_tree;
3276     if (!$org_id) { $org_id = $org_tree->id; }
3277     my $org = $U->find_org($org_tree, $org_id);
3278
3279     return [] unless $org;
3280
3281     my $func = 'unapi.biblio_record_entry_feed';
3282     my $includes = '{holdings_xml,acp,acnp,acns,circ}';
3283     my $limits = "acn=>$limit,acp=>$limit";
3284
3285     if ($is_meta) {
3286         $func = 'unapi.metabib_virtual_record_feed';
3287         $includes = '{holdings_xml,acp,acnp,acns,circ,mmr.unapi}';
3288         $limits .= ",bre=>$limit";
3289     }
3290
3291     my $xml_query = $e->json_query({from => [
3292         $func, '{'.$rec_id.'}', 'marcxml', 
3293         $includes, $org->shortname, $copy_depth, $limits,
3294         undef, undef,undef, undef, undef, 
3295         undef, undef, undef, $pref_ou
3296     ]})->[0];
3297
3298     my $xml = $xml_query->{$func};
3299
3300     my $doc = XML::LibXML->new->parse_string($xml);
3301
3302     my $copies = [];
3303     for my $volume ($doc->documentElement->findnodes('//*[local-name()="volume"]')) {
3304         my $label = $volume->getAttribute('label');
3305         my $prefix = $volume->getElementsByTagName('call_number_prefix')->[0]->getAttribute('label');
3306         my $suffix = $volume->getElementsByTagName('call_number_suffix')->[0]->getAttribute('label');
3307
3308         my $copies_node = $volume->findnodes('./*[local-name()="copies"]')->[0];
3309
3310         for my $copy ($copies_node->findnodes('./*[local-name()="copy"]')) {
3311
3312             my $status = $copy->getElementsByTagName('status')->[0]->textContent;
3313             my $location = $copy->getElementsByTagName('location')->[0]->textContent;
3314             my $circ_lib_sn = $copy->getElementsByTagName('circ_lib')->[0]->getAttribute('shortname');
3315             my $due_date = '';
3316
3317             my $current_circ = $copy->findnodes('./*[local-name()="current_circulation"]')->[0];
3318             if (my $circ = $current_circ->findnodes('./*[local-name()="circ"]')) {
3319                 $due_date = $circ->[0]->getAttribute('due_date');
3320             }
3321
3322             push(@$copies, {
3323                 call_number_label => $label,
3324                 call_number_prefix_label => $prefix,
3325                 call_number_suffix_label => $suffix,
3326                 circ_lib_sn => $circ_lib_sn,
3327                 copy_status => $status,
3328                 copy_location => $location,
3329                 due_date => $due_date
3330             });
3331         }
3332     }
3333
3334     return $copies;
3335 }
3336
3337 sub get_first_call_number {
3338     my ($e, $rec_id, $org_id, $is_staff, $is_meta, $options) = @_;
3339
3340     my $limit = $options->{copy_limit};
3341     $options->{copy_limit} = 1;
3342
3343     my $copies = get_representative_copies(
3344         $e, $rec_id, $org_id, $is_staff, $is_meta, $options);
3345
3346     $options->{copy_limit} = $limit;
3347
3348     return $copies->[0];
3349 }
3350
3351 sub get_one_rec_urls {
3352     my ($self, $e, $org_id, $bib_id) = @_;
3353
3354     my ($resp) = $self->method_lookup(
3355         'open-ils.search.biblio.record.resource_urls.retrieve')
3356         ->run($org_id, $bib_id);
3357
3358     return $resp->{urls};
3359 }
3360
3361 # Start with a bib summary and augment the data with additional
3362 # metarecord content.
3363 sub get_one_metarecord_summary {
3364     my ($self, $e, $org_id, $rec_id) = @_;
3365
3366     my $meta = $e->retrieve_metabib_metarecord($rec_id) or return {};
3367     my $maps = $e->search_metabib_metarecord_source_map({metarecord => $rec_id});
3368
3369     my $bre_id = $meta->master_record; 
3370
3371     my $response = get_one_record_summary($self, $e, $org_id, $bre_id);
3372     $response->{urls} = get_one_rec_urls($self, $e, $org_id, $bre_id);
3373
3374     $response->{metabib_id} = $rec_id;
3375     $response->{metabib_records} = [map {$_->source} @$maps];
3376
3377     # Find the sum of record note counts for all mapped bib records
3378     my @record_ids = map {$_->source} @$maps;
3379     my $notes = $e->search_biblio_record_note({ record => \@record_ids });
3380     my $record_note_count = scalar(@{ $notes });
3381     $response->{record_note_count} = $record_note_count;
3382
3383     my @other_bibs = map {$_->source} grep {$_->source != $bre_id} @$maps;
3384
3385     # Augment the record attributes with those of all of the records
3386     # linked to this metarecord.
3387     if (@other_bibs) {
3388         my $attrs = $e->search_metabib_record_attr_flat({id => \@other_bibs});
3389
3390         my $attributes = $response->{attributes};
3391
3392         for my $attr (@$attrs) {
3393             $attributes->{$attr->attr} = [] unless $attributes->{$attr->attr};
3394             push(@{$attributes->{$attr->attr}}, $attr->value) # avoid dupes
3395                 unless grep {$_ eq $attr->value} @{$attributes->{$attr->attr}};
3396         }
3397     }
3398
3399     return $response;
3400 }
3401
3402 sub get_one_record_summary {
3403     my ($self, $e, $org_id, $rec_id) = @_;
3404
3405     my $bre = $e->retrieve_biblio_record_entry([$rec_id, {
3406         flesh => 1,
3407         flesh_fields => {
3408             bre => [qw/compressed_display_entries mattrs creator editor/]
3409         }
3410     }]) or return {};
3411
3412     # Compressed display fields are packaged as JSON
3413     my $display = {};
3414     $display->{$_->name} = OpenSRF::Utils::JSON->JSON2perl($_->value)
3415         foreach @{$bre->compressed_display_entries};
3416
3417     # Create an object of 'mraf' attributes.
3418     # Any attribute can be multi so dedupe and array-ify all of them.
3419     my $attributes = {};
3420     for my $attr (@{$bre->mattrs}) {
3421         $attributes->{$attr->attr} = {} unless $attributes->{$attr->attr};
3422         $attributes->{$attr->attr}->{$attr->value} = 1; # avoid dupes
3423     }
3424     $attributes->{$_} = [keys %{$attributes->{$_}}] for keys %$attributes;
3425
3426     # Find the count of record notes on this record
3427     my $notes = $e->search_biblio_record_note({ record => $rec_id });
3428     my $record_note_count = scalar(@{ $notes });
3429
3430     # clear bulk
3431     $bre->clear_marc;
3432     $bre->clear_mattrs;
3433     $bre->clear_compressed_display_entries;
3434
3435     return {
3436         id => $rec_id,
3437         record => $bre,
3438         display => $display,
3439         attributes => $attributes,
3440         urls => get_one_rec_urls($self, $e, $org_id, $rec_id),
3441         record_note_count => $record_note_count
3442     };
3443 }
3444
3445 __PACKAGE__->register_method(
3446     method    => 'record_copy_counts_global',
3447     api_name  => 'open-ils.search.biblio.record.copy_counts.global.staff',
3448     signature => {
3449         desc   => q/Returns a count of copies and call numbers for each org
3450                     unit, including items attached to each org unit plus
3451                     a sum of counts for all descendants./,
3452         params => [
3453             {desc => 'Record ID', type => 'number'}
3454         ],
3455         return => {
3456             desc => 'Hash of org unit ID  => {copy: $count, call_number: $id}'
3457         }
3458     }
3459 );
3460
3461 sub record_copy_counts_global {
3462     my ($self, $client, $rec_id) = @_;
3463
3464     my $copies = new_editor()->json_query({
3465         select => {
3466             acp => [{column => 'id', alias => 'copy_id'}, 'circ_lib'],
3467             acn => [{column => 'id', alias => 'cn_id'}, 'owning_lib']
3468         },
3469         from => {acn => {acp => {type => 'left'}}},
3470         where => {
3471             '+acp' => {
3472                 '-or' => [
3473                     {deleted => 'f'},
3474                     {id => undef} # left join
3475                 ]
3476             },
3477             '+acn' => {deleted => 'f', record => $rec_id}
3478         }
3479     });
3480
3481     my $hash = {};
3482     my %seen_cn;
3483
3484     for my $copy (@$copies) {
3485         my $org = $copy->{circ_lib} || $copy->{owning_lib};
3486         $hash->{$org} = {copies => 0, call_numbers => 0} unless $hash->{$org};
3487         $hash->{$org}->{copies}++ if $copy->{circ_lib};
3488
3489         if (!$seen_cn{$copy->{cn_id}}) {
3490             $seen_cn{$copy->{cn_id}} = 1;
3491             $hash->{$org}->{call_numbers}++;
3492         }
3493     }
3494
3495     my $sum;
3496     $sum = sub {
3497         my $node = shift;
3498         my $h = $hash->{$node->id} || {copies => 0, call_numbers => 0};
3499         delete $h->{cn_id};
3500
3501         for my $child (@{$node->children}) {
3502             my $vals = $sum->($child);
3503             $h->{copies} += $vals->{copies};
3504             $h->{call_numbers} += $vals->{call_numbers};
3505         }
3506
3507         $hash->{$node->id} = $h;
3508
3509         return $h;
3510     };
3511
3512     $sum->($U->get_org_tree);
3513
3514     return $hash;
3515 }
3516
3517
3518 1;
3519