]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
LP#1005040: implement business logic
[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
14 use OpenSRF::Utils::Logger qw/:logger/;
15
16
17 use OpenSRF::Utils::JSON;
18
19 use Time::HiRes qw(time);
20 use OpenSRF::EX qw(:try);
21 use Digest::MD5 qw(md5_hex);
22
23 use XML::LibXML;
24 use XML::LibXSLT;
25
26 use Data::Dumper;
27 $Data::Dumper::Indent = 0;
28
29 use OpenILS::Const qw/:const/;
30
31 use OpenILS::Application::AppUtils;
32 my $apputils = "OpenILS::Application::AppUtils";
33 my $U = $apputils;
34
35 my $pfx = "open-ils.search_";
36
37 my $cache;
38 my $cache_timeout;
39 my $superpage_size;
40 my $max_superpages;
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) == '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) = @_;
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);
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) = @_;
1135
1136     my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1137
1138     my $method = $IAmMetabib?
1139         'open-ils.storage.metabib.multiclass.staged.search_fts':
1140         'open-ils.storage.biblio.multiclass.staged.search_fts';
1141
1142     $method .= '.staff' if $self->api_name =~ /staff$/;
1143     $method .= '.atomic';
1144                 
1145     if (!$search_hash{query}) {
1146         return {count => 0} unless (
1147             $search_hash and 
1148             $search_hash->{searches} and 
1149             scalar( keys %{$search_hash->{searches}} ));
1150     }
1151
1152     my $search_duration;
1153     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
1154     my $user_limit  = $search_hash->{limit}  || 10;
1155     my $ignore_facet_classes  = $search_hash->{ignore_facet_classes};
1156     $user_offset = ($user_offset >= 0) ? $user_offset :  0;
1157     $user_limit  = ($user_limit  >= 0) ? $user_limit  : 10;
1158
1159
1160     # we're grabbing results on a per-superpage basis, which means the 
1161     # limit and offset should coincide with superpage boundaries
1162     $search_hash->{offset} = 0;
1163     $search_hash->{limit} = $superpage_size;
1164
1165     # force a well-known check_limit
1166     $search_hash->{check_limit} = $superpage_size; 
1167     # restrict total tested to superpage size * number of superpages
1168     $search_hash->{core_limit}  = $superpage_size * $max_superpages;
1169
1170     # Set the configured estimation strategy, defaults to 'inclusion'.
1171     unless ($estimation_strategy) {
1172         $estimation_strategy = OpenSRF::Utils::SettingsClient
1173             ->new
1174             ->config_value(
1175                 apps => 'open-ils.search', app_settings => 'estimation_strategy'
1176             ) || 'inclusion';
1177     }
1178     $search_hash->{estimation_strategy} = $estimation_strategy;
1179
1180     # pull any existing results from the cache
1181     my $key = search_cache_key($method, $search_hash);
1182     my $facet_key = $key.'_facets';
1183     my $cache_data = $cache->get_cache($key) || {};
1184
1185     # First, we want to make sure that someone else isn't currently trying to perform exactly
1186     # this same search.  The point is to allow just one instance of a search to fill the needs
1187     # of all concurrent, identical searches.  This will avoid spammy searches killing the
1188     # database without requiring admins to start locking some IP addresses out entirely.
1189     #
1190     # There's still a tiny race condition where 2 might run, but without sigificantly more code
1191     # and complexity, this is close to the best we can do.
1192
1193     if ($cache_data->{running}) { # someone is already doing the search...
1194         my $stop_looping = time() + $cache_timeout;
1195         while ( sleep(1) and time() < $stop_looping ) { # sleep for a second ... maybe they'll finish
1196             $cache_data = $cache->get_cache($key) || {};
1197             last if (!$cache_data->{running});
1198         }
1199     } elsif (!$cache_data->{0}) { # we're the first ... let's give it a try
1200         $cache->put_cache($key, { running => $$ }, $cache_timeout / 3);
1201     }
1202
1203     # keep retrieving results until we find enough to 
1204     # fulfill the user-specified limit and offset
1205     my $all_results = [];
1206     my $page; # current superpage
1207     my $est_hit_count = 0;
1208     my $current_page_summary = {};
1209     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1210     my $is_real_hit_count = 0;
1211     my $new_ids = [];
1212
1213     for($page = 0; $page < $max_superpages; $page++) {
1214
1215         my $data = $cache_data->{$page};
1216         my $results;
1217         my $summary;
1218
1219         $logger->debug("staged search: analyzing superpage $page");
1220
1221         if($data) {
1222             # this window of results is already cached
1223             $logger->debug("staged search: found cached results");
1224             $summary = $data->{summary};
1225             $results = $data->{results};
1226
1227         } else {
1228             # retrieve the window of results from the database
1229             $logger->debug("staged search: fetching results from the database");
1230             $search_hash->{skip_check} = $page * $superpage_size;
1231             $search_hash->{return_query} = $page == 0 ? 1 : 0;
1232             my $start = time;
1233             $results = $U->storagereq($method, %$search_hash);
1234             $search_duration = time - $start;
1235             $summary = shift(@$results) if $results;
1236
1237             unless($summary) {
1238                 $logger->info("search timed out: duration=$search_duration: params=".
1239                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1240                 return {count => 0};
1241             }
1242
1243             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1244
1245             my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1246             if($hc == 0) {
1247                 $logger->info("search returned 0 results: duration=$search_duration: params=".
1248                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1249             }
1250
1251             # Create backwards-compatible result structures
1252             if($IAmMetabib) {
1253                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
1254             } else {
1255                 $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
1256             }
1257
1258             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1259             $results = [grep {defined $_->[0]} @$results];
1260             cache_staged_search_page($key, $page, $summary, $results) if $docache;
1261         }
1262
1263         tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib) 
1264             if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1265
1266         $current_page_summary = $summary;
1267
1268         # add the new set of results to the set under construction
1269         push(@$all_results, @$results);
1270
1271         my $current_count = scalar(@$all_results);
1272
1273         if ($page == 0) {
1274             foreach (qw/ query_struct canonicalized_query /) {
1275                 $global_summary->{$_} = $summary->{$_};
1276             }
1277         }
1278
1279         $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1280             if $page == 0;
1281
1282         $logger->debug("staged search: located $current_count, with estimated hits=".
1283             ($summary->{estimated_hit_count} || "none") .
1284             " : visible=" . ($summary->{visible} || "none") . ", checked=" .
1285             ($summary->{checked} || "none")
1286         );
1287
1288         if (defined($summary->{estimated_hit_count})) {
1289             foreach (qw/ checked visible excluded deleted /) {
1290                 $global_summary->{$_} += $summary->{$_};
1291             }
1292             $global_summary->{total} = $summary->{total};
1293         }
1294
1295         # we've found all the possible hits
1296         last if $current_count == $summary->{visible}
1297             and not defined $summary->{estimated_hit_count};
1298
1299         # we've found enough results to satisfy the requested limit/offset
1300         last if $current_count >= ($user_limit + $user_offset);
1301
1302         # we've scanned all possible hits
1303         if($summary->{checked} < $superpage_size) {
1304             $est_hit_count = scalar(@$all_results);
1305             # we have all possible results in hand, so we know the final hit count
1306             $is_real_hit_count = 1;
1307             last;
1308         }
1309     }
1310
1311     # Let other backends grab our data now that we're done.
1312     $cache_data = $cache->get_cache($key);
1313     if ($$cache_data{running} and $$cache_data{running} == $$) {
1314         delete $$cache_data{running};
1315         $cache->put_cache($key, $cache_data, $cache_timeout);
1316     }
1317
1318     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1319
1320     # refine the estimate if we have more than one superpage
1321     if ($page > 0 and not $is_real_hit_count) {
1322         if ($global_summary->{checked} >= $global_summary->{total}) {
1323             $est_hit_count = $global_summary->{visible};
1324         } else {
1325             my $updated_hit_count = $U->storagereq(
1326                 'open-ils.storage.fts_paging_estimate',
1327                 $global_summary->{checked},
1328                 $global_summary->{visible},
1329                 $global_summary->{excluded},
1330                 $global_summary->{deleted},
1331                 $global_summary->{total}
1332             );
1333             $est_hit_count = $updated_hit_count->{$estimation_strategy};
1334         }
1335     }
1336
1337     $conn->respond_complete(
1338         {
1339             global_summary    => $global_summary,
1340             count             => $est_hit_count,
1341             core_limit        => $search_hash->{core_limit},
1342             superpage_size    => $search_hash->{check_limit},
1343             superpage_summary => $current_page_summary,
1344             facet_key         => $facet_key,
1345             ids               => \@results
1346         }
1347     );
1348
1349     $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
1350
1351     return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
1352 }
1353
1354 sub tag_circulated_records {
1355     my ($auth, $results, $metabib) = @_;
1356     my $e = new_editor(authtoken => $auth);
1357     return $results unless $e->checkauth;
1358
1359     my $query = {
1360         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1361         from     => { auch => { acp => { join => 'acn' }} }, 
1362         where    => { usr => $e->requestor->id },
1363         distinct => 1
1364     };
1365
1366     if ($metabib) {
1367         $query = {
1368             select   => { mmrsm => [{ column => 'metarecord', alias => 'tagme' }] },
1369             from     => 'mmrsm',
1370             where    => { source => { in => $query } },
1371             distinct => 1
1372         };
1373     }
1374
1375     # Give me the distinct set of bib records that exist in the user's visible circulation history
1376     my $circ_recs = $e->json_query( $query );
1377
1378     # if the record appears in the circ history, push a 1 onto 
1379     # the rec array structure to indicate truthiness
1380     for my $rec (@$results) {
1381         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1382     }
1383
1384     $results
1385 }
1386
1387 # creates a unique token to represent the query in the cache
1388 sub search_cache_key {
1389     my $method = shift;
1390     my $search_hash = shift;
1391     my @sorted;
1392     for my $key (sort keys %$search_hash) {
1393         push(@sorted, ($key => $$search_hash{$key})) 
1394             unless $key eq 'limit'  or 
1395                    $key eq 'offset' or 
1396                    $key eq 'skip_check';
1397     }
1398     my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1399     return $pfx . md5_hex($method . $s);
1400 }
1401
1402 sub retrieve_cached_facets {
1403     my $self   = shift;
1404     my $client = shift;
1405     my $key    = shift;
1406     my $limit    = shift;
1407
1408     return undef unless ($key and $key =~ /_facets$/);
1409
1410     my $blob = $cache->get_cache($key) || {};
1411
1412     my $facets = {};
1413     if ($limit) {
1414        for my $f ( keys %$blob ) {
1415             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1416             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1417             for my $s ( @sorted ) {
1418                 my ($k) = keys(%$s);
1419                 my ($v) = values(%$s);
1420                 $$facets{$f}{$k} = $v;
1421             }
1422         }
1423     } else {
1424         $facets = $blob;
1425     }
1426
1427     return $facets;
1428 }
1429
1430 __PACKAGE__->register_method(
1431     method   => "retrieve_cached_facets",
1432     api_name => "open-ils.search.facet_cache.retrieve",
1433     signature => {
1434         desc   => 'Returns facet data derived from a specific search based on a key '.
1435                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1436         params => [
1437             {
1438                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1439                 type => 'string',
1440             }
1441         ],
1442         return => {
1443             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1444                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1445                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1446                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1447             type => 'object',
1448         }
1449     }
1450 );
1451
1452
1453 sub cache_facets {
1454     # add facets for this search to the facet cache
1455     my($key, $results, $metabib, $ignore) = @_;
1456     my $data = $cache->get_cache($key);
1457     $data ||= {};
1458
1459     return undef unless (@$results);
1460
1461     my $facets_function = $metabib ? 'search.facets_for_metarecord_set'
1462                                    : 'search.facets_for_record_set';
1463     my $results_str = '{' . join(',', @$results) . '}';
1464     my $ignore_str = ref($ignore) ? '{' . join(',', @$ignore) . '}'
1465                                   : '{}';
1466     my $query = {   
1467         from => [ $facets_function, $ignore_str, $results_str ]
1468     };
1469
1470     my $facets = OpenILS::Utils::CStoreEditor->new->json_query($query, {substream => 1});
1471
1472     for my $facet (@$facets) {
1473         next unless ($facet->{value});
1474         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1475     }
1476
1477     $logger->info("facet compilation: cached with key=$key");
1478
1479     $cache->put_cache($key, $data, $cache_timeout);
1480 }
1481
1482 sub cache_staged_search_page {
1483     # puts this set of results into the cache
1484     my($key, $page, $summary, $results) = @_;
1485     my $data = $cache->get_cache($key);
1486     $data ||= {};
1487     $data->{$page} = {
1488         summary => $summary,
1489         results => $results
1490     };
1491
1492     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1493         ($summary->{estimated_hit_count} || "none") .
1494         ", visible=" . ($summary->{visible} || "none")
1495     );
1496
1497     $cache->put_cache($key, $data, $cache_timeout);
1498 }
1499
1500 sub search_cache {
1501
1502     my $key     = shift;
1503     my $offset  = shift;
1504     my $limit   = shift;
1505     my $start   = $offset;
1506     my $end     = $offset + $limit - 1;
1507
1508     $logger->debug("searching cache for $key : $start..$end\n");
1509
1510     return undef unless $cache;
1511     my $data = $cache->get_cache($key);
1512
1513     return undef unless $data;
1514
1515     my $count = $data->[0];
1516     $data = $data->[1];
1517
1518     return undef unless $offset < $count;
1519
1520     my @result;
1521     for( my $i = $offset; $i <= $end; $i++ ) {
1522         last unless my $d = $$data[$i];
1523         push( @result, $d );
1524     }
1525
1526     $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1527
1528     return \@result;
1529 }
1530
1531
1532 sub put_cache {
1533     my( $key, $count, $data ) = @_;
1534     return undef unless $cache;
1535     $logger->debug("search_cache putting ".
1536         scalar(@$data)." items at key $key with timeout $cache_timeout");
1537     $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1538 }
1539
1540
1541 __PACKAGE__->register_method(
1542     method   => "biblio_mrid_to_modsbatch_batch",
1543     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1544 );
1545
1546 sub biblio_mrid_to_modsbatch_batch {
1547     my( $self, $client, $mrids) = @_;
1548     # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1549     my @mods;
1550     my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1551     for my $id (@$mrids) {
1552         next unless defined $id;
1553         my ($m) = $method->run($id);
1554         push @mods, $m;
1555     }
1556     return \@mods;
1557 }
1558
1559
1560 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1561              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1562     {
1563     __PACKAGE__->register_method(
1564         method    => "biblio_mrid_to_modsbatch",
1565         api_name  => $_,
1566         signature => {
1567             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1568                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1569             params => [
1570                 { desc => 'Metarecord ID', type => 'number' },
1571                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1572             ],
1573             return => {
1574                 desc => 'MVR Object, event on error',
1575             }
1576         }
1577     );
1578 }
1579
1580 sub biblio_mrid_to_modsbatch {
1581     my( $self, $client, $mrid, $args) = @_;
1582
1583     # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1584
1585     my ($mr, $evt) = _grab_metarecord($mrid);
1586     return $evt unless $mr;
1587
1588     my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1589               biblio_mrid_make_modsbatch($self, $client, $mr);
1590
1591     return $mvr unless ref($args);  
1592
1593     # Here we find the lead record appropriate for the given filters 
1594     # and use that for the title and author of the metarecord
1595     my $format = $$args{format};
1596     my $org    = $$args{org};
1597     my $depth  = $$args{depth};
1598
1599     return $mvr unless $format or $org or $depth;
1600
1601     my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1602     $method = "$method.staff" if $self->api_name =~ /staff/o; 
1603
1604     my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1605
1606     if( my $mods = $U->record_to_mvr($rec) ) {
1607
1608         $mvr->title( $mods->title );
1609         $mvr->author($mods->author);
1610         $logger->debug("mods_slim updating title and ".
1611             "author in mvr with ".$mods->title." : ".$mods->author);
1612     }
1613
1614     return $mvr;
1615 }
1616
1617 # converts a metarecord to an mvr
1618 sub _mr_to_mvr {
1619     my $mr = shift;
1620     my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1621     return Fieldmapper::metabib::virtual_record->new($perl);
1622 }
1623
1624 # checks to see if a metarecord has mods, if so returns true;
1625
1626 __PACKAGE__->register_method(
1627     method   => "biblio_mrid_check_mvr",
1628     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1629     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1630               . "if the metarecord already has an mvr associated with it."
1631 );
1632
1633 sub biblio_mrid_check_mvr {
1634     my( $self, $client, $mrid ) = @_;
1635     my $mr; 
1636
1637     my $evt;
1638     if(ref($mrid)) { $mr = $mrid; } 
1639     else { ($mr, $evt) = _grab_metarecord($mrid); }
1640     return $evt if $evt;
1641
1642     # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1643
1644     return _mr_to_mvr($mr) if $mr->mods();
1645     return undef;
1646 }
1647
1648 sub _grab_metarecord {
1649     my $mrid = shift;
1650     my $e = new_editor();
1651     my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1652     return ($mr);
1653 }
1654
1655
1656 __PACKAGE__->register_method(
1657     method   => "biblio_mrid_make_modsbatch",
1658     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1659     notes    => "Takes either a metarecord ID or a metarecord object. "
1660               . "Forces the creations of an mvr for the given metarecord. "
1661               . "The created mvr is returned."
1662 );
1663
1664 sub biblio_mrid_make_modsbatch {
1665     my( $self, $client, $mrid ) = @_;
1666
1667     my $e = new_editor();
1668
1669     my $mr;
1670     if( ref($mrid) ) {
1671         $mr = $mrid;
1672         $mrid = $mr->id;
1673     } else {
1674         $mr = $e->retrieve_metabib_metarecord($mrid) 
1675             or return $e->event;
1676     }
1677
1678     my $masterid = $mr->master_record;
1679     $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1680
1681     my $ids = $U->storagereq(
1682         'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1683     return undef unless @$ids;
1684
1685     my $master = $e->retrieve_biblio_record_entry($masterid)
1686         or return $e->event;
1687
1688     # start the mods batch
1689     my $u = OpenILS::Utils::ModsParser->new();
1690     $u->start_mods_batch( $master->marc );
1691
1692     # grab all of the sub-records and shove them into the batch
1693     my @ids = grep { $_ ne $masterid } @$ids;
1694     #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1695
1696     my $subrecs = [];
1697     if(@$ids) {
1698         for my $i (@$ids) {
1699             my $r = $e->retrieve_biblio_record_entry($i);
1700             push( @$subrecs, $r ) if $r;
1701         }
1702     }
1703
1704     for(@$subrecs) {
1705         $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1706         $u->push_mods_batch( $_->marc ) if $_->marc;
1707     }
1708
1709
1710     # finish up and send to the client
1711     my $mods = $u->finish_mods_batch();
1712     $mods->doc_id($mrid);
1713     $client->respond_complete($mods);
1714
1715
1716     # now update the mods string in the db
1717     my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1718     $mr->mods($string);
1719
1720     $e = new_editor(xact => 1);
1721     $e->update_metabib_metarecord($mr) 
1722         or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1723     $e->finish;
1724
1725     return undef;
1726 }
1727
1728
1729 # converts a mr id into a list of record ids
1730
1731 foreach (qw/open-ils.search.biblio.metarecord_to_records
1732             open-ils.search.biblio.metarecord_to_records.staff/)
1733 {
1734     __PACKAGE__->register_method(
1735         method    => "biblio_mrid_to_record_ids",
1736         api_name  => $_,
1737         signature => {
1738             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1739                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1740             params => [
1741                 { desc => 'Metarecord ID', type => 'number' },
1742                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1743             ],
1744             return => {
1745                 desc => 'Results object like {count => $i, ids =>[...]}',
1746                 type => 'object'
1747             }
1748             
1749         }
1750     );
1751 }
1752
1753 sub biblio_mrid_to_record_ids {
1754     my( $self, $client, $mrid, $args ) = @_;
1755
1756     my $format = $$args{format};
1757     my $org    = $$args{org};
1758     my $depth  = $$args{depth};
1759
1760     my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1761     $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1762     my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1763
1764     return { count => scalar(@$recs), ids => $recs };
1765 }
1766
1767
1768 __PACKAGE__->register_method(
1769     method   => "biblio_record_to_marc_html",
1770     api_name => "open-ils.search.biblio.record.html"
1771 );
1772
1773 __PACKAGE__->register_method(
1774     method   => "biblio_record_to_marc_html",
1775     api_name => "open-ils.search.authority.to_html"
1776 );
1777
1778 # Persistent parsers and setting objects
1779 my $parser = XML::LibXML->new();
1780 my $xslt   = XML::LibXSLT->new();
1781 my $marc_sheet;
1782 my $slim_marc_sheet;
1783 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1784
1785 sub biblio_record_to_marc_html {
1786     my($self, $client, $recordid, $slim, $marcxml) = @_;
1787
1788     my $sheet;
1789     my $dir = $settings_client->config_value("dirs", "xsl");
1790
1791     if($slim) {
1792         unless($slim_marc_sheet) {
1793             my $xsl = $settings_client->config_value(
1794                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1795             if($xsl) {
1796                 $xsl = $parser->parse_file("$dir/$xsl");
1797                 $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1798             }
1799         }
1800         $sheet = $slim_marc_sheet;
1801     }
1802
1803     unless($sheet) {
1804         unless($marc_sheet) {
1805             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1806             my $xsl = $settings_client->config_value(
1807                 "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1808             $xsl = $parser->parse_file("$dir/$xsl");
1809             $marc_sheet = $xslt->parse_stylesheet($xsl);
1810         }
1811         $sheet = $marc_sheet;
1812     }
1813
1814     my $record;
1815     unless($marcxml) {
1816         my $e = new_editor();
1817         if($self->api_name =~ /authority/) {
1818             $record = $e->retrieve_authority_record_entry($recordid)
1819                 or return $e->event;
1820         } else {
1821             $record = $e->retrieve_biblio_record_entry($recordid)
1822                 or return $e->event;
1823         }
1824         $marcxml = $record->marc;
1825     }
1826
1827     my $xmldoc = $parser->parse_string($marcxml);
1828     my $html = $sheet->transform($xmldoc);
1829     return $html->documentElement->toString();
1830 }
1831
1832 __PACKAGE__->register_method(
1833     method    => "format_biblio_record_entry",
1834     api_name  => "open-ils.search.biblio.record.print",
1835     signature => {
1836         desc   => 'Returns a printable version of the specified bib record',
1837         params => [
1838             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1839         ],
1840         return => {
1841             desc => q/An action_trigger.event object or error event./,
1842             type => 'object',
1843         }
1844     }
1845 );
1846 __PACKAGE__->register_method(
1847     method    => "format_biblio_record_entry",
1848     api_name  => "open-ils.search.biblio.record.email",
1849     signature => {
1850         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1851         params => [
1852             { desc => 'Authentication token',  type => 'string'},
1853             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1854         ],
1855         return => {
1856             desc => q/Undefined on success, otherwise an error event./,
1857             type => 'object',
1858         }
1859     }
1860 );
1861
1862 sub format_biblio_record_entry {
1863     my($self, $conn, $arg1, $arg2) = @_;
1864
1865     my $for_print = ($self->api_name =~ /print/);
1866     my $for_email = ($self->api_name =~ /email/);
1867
1868     my $e; my $auth; my $bib_id; my $context_org;
1869
1870     if ($for_print) {
1871         $bib_id = $arg1;
1872         $context_org = $arg2 || $U->get_org_tree->id;
1873         $e = new_editor(xact => 1);
1874     } elsif ($for_email) {
1875         $auth = $arg1;
1876         $bib_id = $arg2;
1877         $e = new_editor(authtoken => $auth, xact => 1);
1878         return $e->die_event unless $e->checkauth;
1879         $context_org = $e->requestor->home_ou;
1880     }
1881
1882     my $bib_ids;
1883     if (ref $bib_id ne 'ARRAY') {
1884         $bib_ids = [ $bib_id ];
1885     } else {
1886         $bib_ids = $bib_id;
1887     }
1888
1889     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1890     $bucket->btype('temp');
1891     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1892     if ($for_email) {
1893         $bucket->owner($e->requestor) 
1894     } else {
1895         $bucket->owner(1);
1896     }
1897     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1898
1899     for my $id (@$bib_ids) {
1900
1901         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1902
1903         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1904         $bucket_entry->target_biblio_record_entry($bib);
1905         $bucket_entry->bucket($bucket_obj->id);
1906         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1907     }
1908
1909     $e->commit;
1910
1911     if ($for_print) {
1912
1913         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1914
1915     } elsif ($for_email) {
1916
1917         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1918     }
1919
1920     return undef;
1921 }
1922
1923
1924 __PACKAGE__->register_method(
1925     method   => "retrieve_all_copy_statuses",
1926     api_name => "open-ils.search.config.copy_status.retrieve.all"
1927 );
1928
1929 sub retrieve_all_copy_statuses {
1930     my( $self, $client ) = @_;
1931     return new_editor()->retrieve_all_config_copy_status();
1932 }
1933
1934
1935 __PACKAGE__->register_method(
1936     method   => "copy_counts_per_org",
1937     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1938 );
1939
1940 __PACKAGE__->register_method(
1941     method   => "copy_counts_per_org",
1942     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1943 );
1944
1945 sub copy_counts_per_org {
1946     my( $self, $client, $record_id ) = @_;
1947
1948     warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1949
1950     my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1951     if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1952
1953     my $counts = $apputils->simple_scalar_request(
1954         "open-ils.storage", $method, $record_id );
1955
1956     $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1957     return $counts;
1958 }
1959
1960
1961 __PACKAGE__->register_method(
1962     method   => "copy_count_summary",
1963     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1964     notes    => "returns an array of these: "
1965               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
1966               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1967 );
1968         
1969
1970 sub copy_count_summary {
1971     my( $self, $client, $rid, $org, $depth ) = @_;
1972     $org   ||= 1;
1973     $depth ||= 0;
1974     my $data = $U->storagereq(
1975         'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1976
1977     return [ sort {
1978         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
1979         cmp
1980         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
1981     } @$data ];
1982 }
1983
1984 __PACKAGE__->register_method(
1985     method   => "copy_location_count_summary",
1986     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1987     notes    => "returns an array of these: "
1988               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, copy_location, <status1_count>, <status2_count>,...] "
1989               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1990 );
1991
1992 sub copy_location_count_summary {
1993     my( $self, $client, $rid, $org, $depth ) = @_;
1994     $org   ||= 1;
1995     $depth ||= 0;
1996     my $data = $U->storagereq(
1997         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1998
1999     return [ sort {
2000         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2001         cmp
2002         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2003
2004         || $a->[4] cmp $b->[4]
2005     } @$data ];
2006 }
2007
2008 __PACKAGE__->register_method(
2009     method   => "copy_count_location_summary",
2010     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
2011     notes    => "returns an array of these: "
2012               . "[ org_id, callnumber_prefix, callnumber_label, callnumber_suffix, <status1_count>, <status2_count>,...] "
2013               . "where statusx is a copy status name.  The statuses are sorted by ID."
2014 );
2015
2016 sub copy_count_location_summary {
2017     my( $self, $client, $rid, $org, $depth ) = @_;
2018     $org   ||= 1;
2019     $depth ||= 0;
2020     my $data = $U->storagereq(
2021         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
2022     return [ sort {
2023         (($a->[1] ? $a->[1] . ' ' : '') . $a->[2] . ($a->[3] ? ' ' . $a->[3] : ''))
2024         cmp
2025         (($b->[1] ? $b->[1] . ' ' : '') . $b->[2] . ($b->[3] ? ' ' . $b->[3] : ''))
2026     } @$data ];
2027 }
2028
2029
2030 foreach (qw/open-ils.search.biblio.marc
2031             open-ils.search.biblio.marc.staff/)
2032 {
2033 __PACKAGE__->register_method(
2034     method    => "marc_search",
2035     api_name  => $_,
2036     signature => {
2037         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
2038                 . 'As usual, the .staff version of the search includes otherwise hidden records',
2039         params => [
2040             {
2041                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
2042                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
2043                 type => 'object'
2044             },
2045             {desc => 'timeout (optional)',  type => 'number'}
2046         ],
2047         return => {
2048             desc => 'Results object like: { "count": $i, "ids": [...] }',
2049             type => 'object'
2050         }
2051     }
2052 );
2053 }
2054
2055 =head3 open-ils.search.biblio.marc (arghash, timeout)
2056
2057 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
2058
2059     searches: complex query object  (required)
2060     org_unit: The org ID to focus the search at
2061     depth   : The org depth     
2062     limit   : integer search limit      default: 10
2063     offset  : integer search offset     default:  0
2064     sort    : What field to sort the results on? [ author | title | pubdate ]
2065     sort_dir: In what direction do we sort? [ asc | desc ]
2066
2067 Additional keys to refine search criteria:
2068
2069     audience : Audience
2070     language : Language (code)
2071     lit_form : Literary form
2072     item_form: Item form
2073     item_type: Item type
2074     format   : The MARC format
2075
2076 Please note that the specific strings to be used in the "addtional keys" will be entirely
2077 dependent on your loaded data.  
2078
2079 All keys except "searches" are optional.
2080 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2081
2082 For example, an arg hash might look like:
2083
2084     $arghash = {
2085         searches => [
2086             {
2087                 term     => "harry",
2088                 restrict => [
2089                     {
2090                         tag => 245,
2091                         subfield => "a"
2092                     }
2093                     # ...
2094                 ]
2095             }
2096             # ...
2097         ],
2098         org_unit  => 1,
2099         limit     => 5,
2100         sort      => "author",
2101         item_type => "g"
2102     }
2103
2104 The arghash is eventually passed to the SRF call:
2105 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2106
2107 Presently, search uses the cache unconditionally.
2108
2109 =cut
2110
2111 # FIXME: that example above isn't actually tested.
2112 # FIXME: sort and limit added.  item_type not tested yet.
2113 # TODO: docache option?
2114 sub marc_search {
2115     my( $self, $conn, $args, $timeout ) = @_;
2116
2117     my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2118     $method .= ".staff" if $self->api_name =~ /staff/;
2119     $method .= ".atomic";
2120
2121     my $limit = $args->{limit} || 10;
2122     my $offset = $args->{offset} || 0;
2123
2124     # allow caller to pass in a call timeout since MARC searches
2125     # can take longer than the default 60-second timeout.  
2126     # Default to 2 mins.  Arbitrarily cap at 5 mins.
2127     $timeout = 120 if !$timeout or $timeout > 300;
2128
2129     my @search;
2130     push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2131     my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2132
2133     my $recs = search_cache($ckey, $offset, $limit);
2134
2135     if(!$recs) {
2136
2137         my $ses = OpenSRF::AppSession->create('open-ils.storage');
2138         my $req = $ses->request($method, %$args);
2139         my $resp = $req->recv($timeout);
2140
2141         if($resp and $recs = $resp->content) {
2142             put_cache($ckey, scalar(@$recs), $recs);
2143         } else {
2144             $recs = [];
2145         }
2146
2147         $ses->kill_me;
2148     }
2149
2150     my $count = 0;
2151     $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2152     my @recs = map { $_->[0] } @$recs;
2153
2154     return { ids => \@recs, count => $count };
2155 }
2156
2157
2158 foreach my $isbn_method (qw/
2159     open-ils.search.biblio.isbn
2160     open-ils.search.biblio.isbn.staff
2161 /) {
2162 __PACKAGE__->register_method(
2163     method    => "biblio_search_isbn",
2164     api_name  => $isbn_method,
2165     signature => {
2166         desc   => 'Retrieve biblio IDs for a given ISBN. The .staff version of the call includes otherwise hidden hits.',
2167         params => [
2168             {desc => 'ISBN', type => 'string'}
2169         ],
2170         return => {
2171             desc => 'Results object like: { "count": $i, "ids": [...] }',
2172             type => 'object'
2173         }
2174     }
2175 );
2176 }
2177
2178 sub biblio_search_isbn { 
2179     my( $self, $client, $isbn ) = @_;
2180     $logger->debug("Searching ISBN $isbn");
2181     # the previous implementation of this method was essentially unlimited,
2182     # so we will set our limit very high and let multiclass.query provide any
2183     # actual limit
2184     # XXX: if making this unlimited is deemed important, we might consider
2185     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2186     # which is functionally deprecated at this point, or a custom call to
2187     # 'open-ils.storage.biblio.multiclass.search_fts'
2188
2189     my $isbn_method = 'open-ils.search.biblio.multiclass.query';
2190     if ($self->api_name =~ m/.staff$/) {
2191         $isbn_method .= '.staff';
2192     }
2193
2194     my $method = $self->method_lookup($isbn_method);
2195     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2196     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2197     return { ids => \@recs, count => $search_result->{'count'} };
2198 }
2199
2200 __PACKAGE__->register_method(
2201     method   => "biblio_search_isbn_batch",
2202     api_name => "open-ils.search.biblio.isbn_list",
2203 );
2204
2205 # XXX: see biblio_search_isbn() for note concerning 'limit'
2206 sub biblio_search_isbn_batch { 
2207     my( $self, $client, $isbn_list ) = @_;
2208     $logger->debug("Searching ISBNs @$isbn_list");
2209     my @recs = (); my %rec_set = ();
2210     my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2211     foreach my $isbn ( @$isbn_list ) {
2212         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2213         my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2214         foreach my $rec (@recs_subset) {
2215             if (! $rec_set{ $rec }) {
2216                 $rec_set{ $rec } = 1;
2217                 push @recs, $rec;
2218             }
2219         }
2220     }
2221     return { ids => \@recs, count => scalar(@recs) };
2222 }
2223
2224 foreach my $issn_method (qw/
2225     open-ils.search.biblio.issn
2226     open-ils.search.biblio.issn.staff
2227 /) {
2228 __PACKAGE__->register_method(
2229     method   => "biblio_search_issn",
2230     api_name => $issn_method,
2231     signature => {
2232         desc   => 'Retrieve biblio IDs for a given ISSN',
2233         params => [
2234             {desc => 'ISBN', type => 'string'}
2235         ],
2236         return => {
2237             desc => 'Results object like: { "count": $i, "ids": [...] }',
2238             type => 'object'
2239         }
2240     }
2241 );
2242 }
2243
2244 sub biblio_search_issn { 
2245     my( $self, $client, $issn ) = @_;
2246     $logger->debug("Searching ISSN $issn");
2247     # the previous implementation of this method was essentially unlimited,
2248     # so we will set our limit very high and let multiclass.query provide any
2249     # actual limit
2250     # XXX: if making this unlimited is deemed important, we might consider
2251     # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2252     # which is functionally deprecated at this point, or a custom call to
2253     # 'open-ils.storage.biblio.multiclass.search_fts'
2254
2255     my $issn_method = 'open-ils.search.biblio.multiclass.query';
2256     if ($self->api_name =~ m/.staff$/) {
2257         $issn_method .= '.staff';
2258     }
2259
2260     my $method = $self->method_lookup($issn_method);
2261     my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2262     my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2263     return { ids => \@recs, count => $search_result->{'count'} };
2264 }
2265
2266
2267 __PACKAGE__->register_method(
2268     method    => "fetch_mods_by_copy",
2269     api_name  => "open-ils.search.biblio.mods_from_copy",
2270     argc      => 1,
2271     signature => {
2272         desc    => 'Retrieve MODS record given an attached copy ID',
2273         params  => [
2274             { desc => 'Copy ID', type => 'number' }
2275         ],
2276         returns => {
2277             desc => 'MODS record, event on error or uncataloged item'
2278         }
2279     }
2280 );
2281
2282 sub fetch_mods_by_copy {
2283     my( $self, $client, $copyid ) = @_;
2284     my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2285     return $evt if $evt;
2286     return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2287     return $apputils->record_to_mvr($record);
2288 }
2289
2290
2291 # -------------------------------------------------------------------------------------
2292
2293 __PACKAGE__->register_method(
2294     method   => "cn_browse",
2295     api_name => "open-ils.search.callnumber.browse.target",
2296     notes    => "Starts a callnumber browse"
2297 );
2298
2299 __PACKAGE__->register_method(
2300     method   => "cn_browse",
2301     api_name => "open-ils.search.callnumber.browse.page_up",
2302     notes    => "Returns the previous page of callnumbers",
2303 );
2304
2305 __PACKAGE__->register_method(
2306     method   => "cn_browse",
2307     api_name => "open-ils.search.callnumber.browse.page_down",
2308     notes    => "Returns the next page of callnumbers",
2309 );
2310
2311
2312 # RETURNS array of arrays like so: label, owning_lib, record, id
2313 sub cn_browse {
2314     my( $self, $client, @params ) = @_;
2315     my $method;
2316
2317     $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2318         if( $self->api_name =~ /target/ );
2319     $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2320         if( $self->api_name =~ /page_up/ );
2321     $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2322         if( $self->api_name =~ /page_down/ );
2323
2324     return $apputils->simplereq( 'open-ils.storage', $method, @params );
2325 }
2326 # -------------------------------------------------------------------------------------
2327
2328 __PACKAGE__->register_method(
2329     method        => "fetch_cn",
2330     api_name      => "open-ils.search.callnumber.retrieve",
2331     authoritative => 1,
2332     notes         => "retrieves a callnumber based on ID",
2333 );
2334
2335 sub fetch_cn {
2336     my( $self, $client, $id ) = @_;
2337
2338     my $e = new_editor();
2339     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 0, $e );
2340     return $evt if $evt;
2341     return $cn;
2342 }
2343
2344 __PACKAGE__->register_method(
2345     method        => "fetch_fleshed_cn",
2346     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2347     authoritative => 1,
2348     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2349 );
2350
2351 sub fetch_fleshed_cn {
2352     my( $self, $client, $id ) = @_;
2353
2354     my $e = new_editor();
2355     my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1, $e );
2356     return $evt if $evt;
2357     return $cn;
2358 }
2359
2360
2361 __PACKAGE__->register_method(
2362     method    => "fetch_copy_by_cn",
2363     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2364     signature => q/
2365         Returns an array of copy ID's by callnumber ID
2366         @param cnid The callnumber ID
2367         @return An array of copy IDs
2368     /
2369 );
2370
2371 sub fetch_copy_by_cn {
2372     my( $self, $conn, $cnid ) = @_;
2373     return $U->cstorereq(
2374         'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2375         { call_number => $cnid, deleted => 'f' } );
2376 }
2377
2378 __PACKAGE__->register_method(
2379     method    => 'fetch_cn_by_info',
2380     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2381     signature => q/
2382         @param label The callnumber label
2383         @param record The record the cn is attached to
2384         @param org The owning library of the cn
2385         @return The callnumber object
2386     /
2387 );
2388
2389
2390 sub fetch_cn_by_info {
2391     my( $self, $conn, $label, $record, $org ) = @_;
2392     return $U->cstorereq(
2393         'open-ils.cstore.direct.asset.call_number.search',
2394         { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2395 }
2396
2397
2398
2399 __PACKAGE__->register_method(
2400     method   => 'bib_extras',
2401     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2402     ctype => 'lit_form'
2403 );
2404 __PACKAGE__->register_method(
2405     method   => 'bib_extras',
2406     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2407     ctype => 'item_form'
2408 );
2409 __PACKAGE__->register_method(
2410     method   => 'bib_extras',
2411     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2412     ctype => 'item_type',
2413 );
2414 __PACKAGE__->register_method(
2415     method   => 'bib_extras',
2416     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2417     ctype => 'bib_level'
2418 );
2419 __PACKAGE__->register_method(
2420     method   => 'bib_extras',
2421     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2422     ctype => 'audience'
2423 );
2424
2425 sub bib_extras {
2426     my $self = shift;
2427     $logger->warn("deprecation warning: " .$self->api_name);
2428
2429     my $e = new_editor();
2430
2431     my $ctype = $self->{ctype};
2432     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2433
2434     my @objs;
2435     for my $ccvm (@$ccvms) {
2436         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2437         $obj->value($ccvm->value);
2438         $obj->code($ccvm->code);
2439         $obj->description($ccvm->description) if $obj->can('description');
2440         push(@objs, $obj);
2441     }
2442
2443     return \@objs;
2444 }
2445
2446
2447
2448 __PACKAGE__->register_method(
2449     method    => 'fetch_slim_record',
2450     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2451     signature => {
2452         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2453         params => [
2454             { desc => 'Array of Record IDs', type => 'array' }
2455         ],
2456         return => { 
2457             desc => 'Array of biblio records, event on error'
2458         }
2459     }
2460 );
2461
2462 sub fetch_slim_record {
2463     my( $self, $conn, $ids ) = @_;
2464
2465     my $editor = new_editor();
2466     my @res;
2467     for( @$ids ) {
2468         return $editor->event unless
2469             my $r = $editor->retrieve_biblio_record_entry($_);
2470         $r->clear_marc;
2471         push(@res, $r);
2472     }
2473     return \@res;
2474 }
2475
2476 __PACKAGE__->register_method(
2477     method    => 'rec_hold_parts',
2478     api_name  => 'open-ils.search.biblio.record_hold_parts',
2479     signature => q/
2480        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2481     /
2482 );
2483
2484 sub rec_hold_parts {
2485     my( $self, $conn, $args ) = @_;
2486
2487     my $rec        = $$args{record};
2488     my $mrec       = $$args{metarecord};
2489     my $pickup_lib = $$args{pickup_lib};
2490     my $e = new_editor();
2491
2492     my $query = {
2493         select => {bmp => ['id', 'label']},
2494         from => 'bmp',
2495         where => {
2496             id => {
2497                 in => {
2498                     select => {'acpm' => ['part']},
2499                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2500                     where => {
2501                         '+acp' => {'deleted' => 'f'},
2502                         '+bre' => {id => $rec}
2503                     },
2504                     distinct => 1,
2505                 }
2506             },
2507             deleted => 'f'
2508         },
2509         order_by =>[{class=>'bmp', field=>'label_sortkey'}]
2510     };
2511
2512     if(defined $pickup_lib) {
2513         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2514         if($hard_boundary) {
2515             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2516             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2517         }
2518     }
2519
2520     return $e->json_query($query);
2521 }
2522
2523
2524
2525
2526 __PACKAGE__->register_method(
2527     method    => 'rec_to_mr_rec_descriptors',
2528     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2529     signature => q/
2530         specialized method...
2531         Given a biblio record id or a metarecord id, 
2532         this returns a list of metabib.record_descriptor
2533         objects that live within the same metarecord
2534         @param args Object of args including:
2535     /
2536 );
2537
2538 sub rec_to_mr_rec_descriptors {
2539     my( $self, $conn, $args ) = @_;
2540
2541     my $rec        = $$args{record};
2542     my $mrec       = $$args{metarecord};
2543     my $item_forms = $$args{item_forms};
2544     my $item_types = $$args{item_types};
2545     my $item_lang  = $$args{item_lang};
2546     my $pickup_lib = $$args{pickup_lib};
2547
2548     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2549
2550     my $e = new_editor();
2551     my $recs;
2552
2553     if( !$mrec ) {
2554         my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2555         return $e->event unless @$map;
2556         $mrec = $$map[0]->metarecord;
2557     }
2558
2559     $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2560     return $e->event unless @$recs;
2561
2562     my @recs = map { $_->source } @$recs;
2563     my $search = { record => \@recs };
2564     $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2565     $search->{item_type} = $item_types if $item_types and @$item_types;
2566     $search->{item_lang} = $item_lang  if $item_lang;
2567
2568     my $desc = $e->search_metabib_record_descriptor($search);
2569
2570     my $query = {
2571         distinct => 1,
2572         select   => { 'bre' => ['id'] },
2573         from     => {
2574             'bre' => {
2575                 'acn' => {
2576                     'join' => {
2577                         'acp' => {"join" => {"acpl" => {}, "ccs" => {}}}
2578                       }
2579                   }
2580              }
2581         },
2582         where => {
2583             '+bre' => { id => \@recs },
2584             '+acp' => {
2585                 holdable => 't',
2586                 deleted  => 'f'
2587             },
2588             "+ccs" => { holdable => 't' },
2589             "+acpl" => { holdable => 't', deleted => 'f' }
2590         }
2591     };
2592
2593     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2594         my $orgs = $e->json_query(
2595             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2596         ) or return $e->die_event;
2597
2598         $query->{where}->{"+acp"}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2599     }
2600
2601     my $good_records = $e->json_query($query) or return $e->die_event;
2602
2603     my @keep;
2604     for my $d (@$desc) {
2605         if ( grep { $d->record == $_->{id} } @$good_records ) {
2606             push @keep, $d;
2607         }
2608     }
2609
2610     $desc = \@keep;
2611
2612     return { metarecord => $mrec, descriptors => $desc };
2613 }
2614
2615
2616 __PACKAGE__->register_method(
2617     method   => 'fetch_age_protect',
2618     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2619 );
2620
2621 sub fetch_age_protect {
2622     return new_editor()->retrieve_all_config_rule_age_hold_protect();
2623 }
2624
2625
2626 __PACKAGE__->register_method(
2627     method   => 'copies_by_cn_label',
2628     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2629 );
2630
2631 __PACKAGE__->register_method(
2632     method   => 'copies_by_cn_label',
2633     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2634 );
2635
2636 sub copies_by_cn_label {
2637     my( $self, $conn, $record, $cn_parts, $circ_lib ) = @_;
2638     my $e = new_editor();
2639     my $cnp_id = $cn_parts->[0] eq '' ? -1 : $e->search_asset_call_number_prefix({label => $cn_parts->[0]}, {idlist=>1})->[0];
2640     my $cns_id = $cn_parts->[2] eq '' ? -1 : $e->search_asset_call_number_suffix({label => $cn_parts->[2]}, {idlist=>1})->[0];
2641     my $cns = $e->search_asset_call_number({record => $record, prefix => $cnp_id, label => $cn_parts->[1], suffix => $cns_id, deleted => 'f'}, {idlist=>1});
2642     return [] unless @$cns;
2643
2644     # show all non-deleted copies in the staff client ...
2645     if ($self->api_name =~ /staff$/o) {
2646         return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2647     }
2648
2649     # ... otherwise, grab the copies ...
2650     my $copies = $e->search_asset_copy(
2651         [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2652           {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2653         ]
2654     );
2655
2656     # ... and test for location and status visibility
2657     return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2658 }
2659
2660
2661 1;
2662