]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Search/Biblio.pm
OU locale lookup needs a bit more work, it needs to pass a language instead of an...
[working/Evergreen.git] / Open-ILS / src / perlmods / 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
280 __PACKAGE__->register_method(
281     method   => "biblio_search_tcn",
282     api_name => "open-ils.search.biblio.tcn",
283     argc     => 1,
284     signature => {
285         desc   => "Retrieve related record ID(s) given a TCN",
286         params => [
287             { desc => 'TCN', type => 'string' },
288             { desc => 'Flag indicating to include deleted records', type => 'string' }
289         ],
290         return => {
291             desc => 'Results object like: { "count": $i, "ids": [...] }',
292             type => 'object'
293         }
294     }
295
296 );
297
298 sub biblio_search_tcn {
299
300     my( $self, $client, $tcn, $include_deleted ) = @_;
301
302     $tcn =~ s/^\s+|\s+$//og;
303
304     my $e = new_editor();
305     my $search = {tcn_value => $tcn};
306     $search->{deleted} = 'f' unless $include_deleted;
307     my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
308         
309     return { count => scalar(@$recs), ids => $recs };
310 }
311
312
313 # --------------------------------------------------------------------------------
314
315 __PACKAGE__->register_method(
316     method   => "biblio_barcode_to_copy",
317     api_name => "open-ils.search.asset.copy.find_by_barcode",
318 );
319 sub biblio_barcode_to_copy { 
320         my( $self, $client, $barcode ) = @_;
321         my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
322         return $evt if $evt;
323         return $copy;
324 }
325
326 __PACKAGE__->register_method(
327     method   => "biblio_id_to_copy",
328     api_name => "open-ils.search.asset.copy.batch.retrieve",
329 );
330 sub biblio_id_to_copy { 
331         my( $self, $client, $ids ) = @_;
332         $logger->info("Fetching copies @$ids");
333         return $U->cstorereq(
334                 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
335 }
336
337
338 __PACKAGE__->register_method(
339         method  => "biblio_id_to_uris",
340         api_name=> "open-ils.search.asset.uri.retrieve_by_bib",
341         argc    => 2, 
342     stream  => 1,
343     signature => q#
344         @param BibID Which bib record contains the URIs
345         @param OrgID Where to look for URIs
346         @param OrgDepth Range adjustment for OrgID
347         @return A stream or list of 'auri' objects
348     #
349
350 );
351 sub biblio_id_to_uris { 
352         my( $self, $client, $bib, $org, $depth ) = @_;
353     die "Org ID required" unless defined($org);
354     die "Bib ID required" unless defined($bib);
355
356     my @params;
357     push @params, $depth if (defined $depth);
358
359         my $ids = $U->cstorereq( "open-ils.cstore.json_query.atomic",
360         {   select  => { auri => [ 'id' ] },
361             from    => {
362                 acn => {
363                     auricnm => {
364                         field   => 'call_number',
365                         fkey    => 'id',
366                         join    => {
367                             auri    => {
368                                 field => 'id',
369                                 fkey => 'uri',
370                                 filter  => { active => 't' }
371                             }
372                         }
373                     }
374                 }
375             },
376             where   => {
377                 '+acn'  => {
378                     record      => $bib,
379                     owning_lib  => {
380                         in  => {
381                             select  => { aou => [ { column => 'id', transform => 'actor.org_unit_descendants', params => \@params, result_field => 'id' } ] },
382                             from    => 'aou',
383                             where   => { id => $org },
384                             distinct=> 1
385                         }
386                     }
387                 }
388             },
389             distinct=> 1,
390         }
391     );
392
393         my $uris = $U->cstorereq(
394                 "open-ils.cstore.direct.asset.uri.search.atomic",
395         { id => [ map { (values %$_) } @$ids ] }
396     );
397
398     $client->respond($_) for (@$uris);
399
400     return undef;
401 }
402
403
404 __PACKAGE__->register_method(
405     method    => "copy_retrieve",
406     api_name  => "open-ils.search.asset.copy.retrieve",
407     argc      => 1,
408     signature => {
409         desc   => 'Retrieve a copy object based on the Copy ID',
410         params => [
411             { desc => 'Copy ID', type => 'number'}
412         ],
413         return => {
414             desc => 'Copy object, event on error'
415         }
416     }
417 );
418
419 sub copy_retrieve {
420         my( $self, $client, $cid ) = @_;
421         my( $copy, $evt ) = $U->fetch_copy($cid);
422         return $evt || $copy;
423 }
424
425 __PACKAGE__->register_method(
426     method   => "volume_retrieve",
427     api_name => "open-ils.search.asset.call_number.retrieve"
428 );
429 sub volume_retrieve {
430         my( $self, $client, $vid ) = @_;
431         my $e = new_editor();
432         my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
433         return $vol;
434 }
435
436 __PACKAGE__->register_method(
437     method        => "fleshed_copy_retrieve_batch",
438     api_name      => "open-ils.search.asset.copy.fleshed.batch.retrieve",
439     authoritative => 1,
440 );
441
442 sub fleshed_copy_retrieve_batch { 
443         my( $self, $client, $ids ) = @_;
444         $logger->info("Fetching fleshed copies @$ids");
445         return $U->cstorereq(
446                 "open-ils.cstore.direct.asset.copy.search.atomic",
447                 { id => $ids },
448                 { flesh => 1, 
449                   flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
450                 });
451 }
452
453
454 __PACKAGE__->register_method(
455     method   => "fleshed_copy_retrieve",
456     api_name => "open-ils.search.asset.copy.fleshed.retrieve",
457 );
458
459 sub fleshed_copy_retrieve { 
460         my( $self, $client, $id ) = @_;
461         my( $c, $e) = $U->fetch_fleshed_copy($id);
462         return $e || $c;
463 }
464
465
466 __PACKAGE__->register_method(
467     method        => 'fleshed_by_barcode',
468     api_name      => "open-ils.search.asset.copy.fleshed2.find_by_barcode",
469     authoritative => 1,
470 );
471 sub fleshed_by_barcode {
472         my( $self, $conn, $barcode ) = @_;
473         my $e = new_editor();
474         my $copyid = $e->search_asset_copy(
475                 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
476                 or return $e->event;
477         return fleshed_copy_retrieve2( $self, $conn, $copyid);
478 }
479
480
481 __PACKAGE__->register_method(
482     method        => "fleshed_copy_retrieve2",
483     api_name      => "open-ils.search.asset.copy.fleshed2.retrieve",
484     authoritative => 1,
485 );
486
487 sub fleshed_copy_retrieve2 { 
488         my( $self, $client, $id ) = @_;
489         my $e = new_editor();
490         my $copy = $e->retrieve_asset_copy(
491                 [
492                         $id,
493             {
494                 flesh        => 2,
495                 flesh_fields => {
496                     acp => [
497                         qw/ location status stat_cat_entry_copy_maps notes age_protect /
498                     ],
499                     ascecm => [qw/ stat_cat stat_cat_entry /],
500                 }
501             }
502                 ]
503         ) or return $e->event;
504
505         # For backwards compatibility
506         #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
507
508         if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
509                 $copy->circulations(
510                         $e->search_action_circulation( 
511                                 [       
512                                         { target_copy => $copy->id },
513                                         {
514                                                 order_by => { circ => 'xact_start desc' },
515                                                 limit => 1
516                                         }
517                                 ]
518                         )
519                 );
520         }
521
522         return $copy;
523 }
524
525
526 __PACKAGE__->register_method(
527     method        => 'flesh_copy_custom',
528     api_name      => 'open-ils.search.asset.copy.fleshed.custom',
529     authoritative => 1,
530 );
531
532 sub flesh_copy_custom {
533         my( $self, $conn, $copyid, $fields ) = @_;
534         my $e = new_editor();
535         my $copy = $e->retrieve_asset_copy(
536                 [
537                         $copyid,
538                         { 
539                                 flesh                           => 1,
540                                 flesh_fields    => { 
541                                         acp => $fields,
542                                 }
543                         }
544                 ]
545         ) or return $e->event;
546         return $copy;
547 }
548
549
550 __PACKAGE__->register_method(
551     method   => "biblio_barcode_to_title",
552     api_name => "open-ils.search.biblio.find_by_barcode",
553 );
554
555 sub biblio_barcode_to_title {
556         my( $self, $client, $barcode ) = @_;
557
558         my $title = $apputils->simple_scalar_request(
559                 "open-ils.storage",
560                 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
561
562         return { ids => [ $title->id ], count => 1 } if $title;
563         return { count => 0 };
564 }
565
566 __PACKAGE__->register_method(
567     method        => 'title_id_by_item_barcode',
568     api_name      => 'open-ils.search.bib_id.by_barcode',
569     authoritative => 1,
570     signature => { 
571         desc   => 'Retrieve copy object with fleshed record, given the barcode',
572         params => [
573             { desc => 'Item barcode', type => 'string' }
574         ],
575         return => {
576             desc => 'Asset copy object with fleshed record and callnumber, or event on error or null set'
577         }
578     }
579 );
580
581 sub title_id_by_item_barcode {
582     my( $self, $conn, $barcode ) = @_;
583     my $e = new_editor();
584     my $copies = $e->search_asset_copy(
585         [
586             { deleted => 'f', barcode => $barcode },
587             {
588                 flesh => 2,
589                 flesh_fields => {
590                     acp => [ 'call_number' ],
591                     acn => [ 'record' ]
592                 }
593             }
594         ]
595     );
596
597     return $e->event unless @$copies;
598     return $$copies[0]->call_number->record->id;
599 }
600
601
602 __PACKAGE__->register_method(
603     method   => "biblio_copy_to_mods",
604     api_name => "open-ils.search.biblio.copy.mods.retrieve",
605 );
606
607 # takes a copy object and returns it fleshed mods object
608 sub biblio_copy_to_mods {
609         my( $self, $client, $copy ) = @_;
610
611         my $volume = $U->cstorereq( 
612                 "open-ils.cstore.direct.asset.call_number.retrieve",
613                 $copy->call_number() );
614
615         my $mods = _records_to_mods($volume->record());
616         $mods = shift @$mods;
617         $volume->copies([$copy]);
618         push @{$mods->call_numbers()}, $volume;
619
620         return $mods;
621 }
622
623
624 =head1 NAME
625
626 OpenILS::Application::Search::Biblio
627
628 =head1 DESCRIPTION
629
630 =head2 API METHODS
631
632 =head3 open-ils.search.biblio.multiclass.query (arghash, query, docache)
633
634 For arghash and docache, see B<open-ils.search.biblio.multiclass>.
635
636 The query argument is a string, but built like a hash with key: value pairs.
637 Recognized search keys include: 
638
639  keyword (kw) - search keyword(s) *
640  author  (au) - search author(s)  *
641  name    (au) - same as author    *
642  title   (ti) - search title      *
643  subject (su) - search subject    *
644  series  (se) - search series     *
645  lang - limit by language (specifiy multiple langs with lang:l1 lang:l2 ...)
646  site - search at specified org unit, corresponds to actor.org_unit.shortname
647  sort - sort type (title, author, pubdate)
648  dir  - sort direction (asc, desc)
649  available - if set to anything other than "false" or "0", limits to available items
650
651 * Searching keyword, author, title, subject, and series supports additional search 
652 subclasses, specified with a "|".  For example, C<title|proper:gone with the wind>.
653
654 For more, see B<config.metabib_field>.
655
656 =cut
657
658 foreach (qw/open-ils.search.biblio.multiclass.query
659             open-ils.search.biblio.multiclass.query.staff
660             open-ils.search.metabib.multiclass.query
661             open-ils.search.metabib.multiclass.query.staff/)
662 {
663 __PACKAGE__->register_method(
664     api_name  => $_,
665     method    => 'multiclass_query',
666     signature => {
667         desc   => 'Perform a search query.  The .staff version of the call includes otherwise hidden hits.',
668         params => [
669             {name => 'arghash', desc => 'Arg hash (see open-ils.search.biblio.multiclass)',         type => 'object'},
670             {name => 'query',   desc => 'Raw human-readable query (see perldoc '. __PACKAGE__ .')', type => 'string'},
671             {name => 'docache', desc => 'Flag for caching (see open-ils.search.biblio.multiclass)', type => 'object'},
672         ],
673         return => {
674             desc => 'Search results from query, like: { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
675             type => 'object',       # TODO: update as miker's new elements are included
676         }
677     }
678 );
679 }
680
681 sub multiclass_query {
682     my($self, $conn, $arghash, $query, $docache) = @_;
683
684     $logger->debug("initial search query => $query");
685     my $orig_query = $query;
686
687     $query =~ s/\+/ /go;
688     $query =~ s/'/ /go;
689     $query =~ s/^\s+//go;
690
691     # convert convenience classes (e.g. kw for keyword) to the full class name
692     $query =~ s/kw(:|\|)/keyword$1/go;
693     $query =~ s/ti(:|\|)/title$1/go;
694     $query =~ s/au(:|\|)/author$1/go;
695     $query =~ s/su(:|\|)/subject$1/go;
696     $query =~ s/se(:|\|)/series$1/go;
697     $query =~ s/name(:|\|)/author$1/og;
698
699     $logger->debug("cleansed query string => $query");
700     my $search = {};
701
702     my $simple_class_re  = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
703     my $class_list_re    = qr/(?:keyword|title|author|subject|series)/;
704     my $modifier_list_re = qr/(?:site|dir|sort|lang|available)/;
705
706     my $tmp_value = '';
707     while ($query =~ s/$simple_class_re//so) {
708
709         my $qpart = $1;
710         my $where = index($qpart,':');
711         my $type  = substr($qpart, 0, $where++);
712         my $value = substr($qpart, $where);
713
714         if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
715             $tmp_value = "$qpart $tmp_value";
716             next;
717         }
718
719         if ($type =~ /$class_list_re/o ) {
720             $value .= $tmp_value;
721             $tmp_value = '';
722         }
723
724         next unless $type and $value;
725
726         $value =~ s/^\s*//og;
727         $value =~ s/\s*$//og;
728         $type = 'sort_dir' if $type eq 'dir';
729
730         if($type eq 'site') {
731             # 'site' is the org shortname.  when using this, we also want 
732             # to search at the requested org's depth
733             my $e = new_editor();
734             if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
735                 $arghash->{org_unit} = $org->id if $org;
736                 $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
737             } else {
738                 $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
739             }
740
741         } elsif($type eq 'available') {
742             # limit to available
743             $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
744
745         } elsif($type eq 'lang') {
746             # collect languages into an array of languages
747             $arghash->{language} = [] unless $arghash->{language};
748             push(@{$arghash->{language}}, $value);
749
750         } elsif($type =~ /^sort/o) {
751             # sort and sort_dir modifiers
752             $arghash->{$type} = $value;
753
754         } else {
755             # append the search term to the term under construction
756             $search->{$type} =  {} unless $search->{$type};
757             $search->{$type}->{term} =  
758                 ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
759         }
760     }
761
762     $query .= " $tmp_value";
763     $query =~ s/\s+/ /go;
764     $query =~ s/^\s+//go;
765     $query =~ s/\s+$//go;
766
767     my $type = $arghash->{default_class} || 'keyword';
768     $type = ($type eq '-') ? 'keyword' : $type;
769     $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
770
771     if($query) {
772         # This is the front part of the string before any special tokens were
773         # parsed OR colon-separated strings that do not denote a class.
774         # Add this data to the default search class
775         $search->{$type} =  {} unless $search->{$type};
776         $search->{$type}->{term} =
777             ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
778     }
779     my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
780
781     # capture the original limit because the search method alters the limit internally
782     my $ol = $arghash->{limit};
783
784         my $sclient = OpenSRF::Utils::SettingsClient->new;
785
786     (my $method = $self->api_name) =~ s/\.query//o;
787
788     $method =~ s/multiclass/multiclass.staged/
789         if $sclient->config_value(apps => 'open-ils.search',
790             app_settings => 'use_staged_search') =~ /true/i;
791
792     # XXX This stops the session locale from doing the right thing.
793     # XXX Revisit this and have it translate to a lang instead of a locale.
794     #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
795     #    unless $arghash->{preferred_language};
796
797         $method = $self->method_lookup($method);
798     my ($data) = $method->run($arghash, $docache);
799
800     $arghash->{searches} = $search if (!$data->{complex_query});
801
802     $arghash->{limit} = $ol if $ol;
803     $data->{compiled_search} = $arghash;
804     $data->{query} = $orig_query;
805
806     $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
807
808     return $data;
809 }
810
811 __PACKAGE__->register_method(
812     method    => 'cat_search_z_style_wrapper',
813     api_name  => 'open-ils.search.biblio.zstyle',
814     stream    => 1,
815     signature => q/@see open-ils.search.biblio.multiclass/
816 );
817
818 __PACKAGE__->register_method(
819     method    => 'cat_search_z_style_wrapper',
820     api_name  => 'open-ils.search.biblio.zstyle.staff',
821     stream    => 1,
822     signature => q/@see open-ils.search.biblio.multiclass/
823 );
824
825 sub cat_search_z_style_wrapper {
826         my $self = shift;
827         my $client = shift;
828         my $authtoken = shift;
829         my $args = shift;
830
831         my $cstore = OpenSRF::AppSession->connect('open-ils.cstore');
832
833         my $ou = $cstore->request(
834                 'open-ils.cstore.direct.actor.org_unit.search',
835                 { parent_ou => undef }
836         )->gather(1);
837
838         my $result = { service => 'native-evergreen-catalog', records => [] };
839         my $searchhash = { limit => $$args{limit}, offset => $$args{offset}, org_unit => $ou->id };
840
841         $$searchhash{searches}{title}{term}   = $$args{search}{title}   if $$args{search}{title};
842         $$searchhash{searches}{author}{term}  = $$args{search}{author}  if $$args{search}{author};
843         $$searchhash{searches}{subject}{term} = $$args{search}{subject} if $$args{search}{subject};
844         $$searchhash{searches}{keyword}{term} = $$args{search}{keyword} if $$args{search}{keyword};
845
846         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{tcn}       if $$args{search}{tcn};
847         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{isbn}      if $$args{search}{isbn};
848         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{issn}      if $$args{search}{issn};
849         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{publisher} if $$args{search}{publisher};
850         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{pubdate}   if $$args{search}{pubdate};
851         $$searchhash{searches}{keyword}{term} .= join ' ', $$searchhash{searches}{keyword}{term}, $$args{search}{item_type} if $$args{search}{item_type};
852
853         my $list = the_quest_for_knowledge( $self, $client, $searchhash );
854
855         if ($list->{count} > 0) {
856                 $result->{count} = $list->{count};
857
858                 my $records = $cstore->request(
859                         'open-ils.cstore.direct.biblio.record_entry.search.atomic',
860                         { id => [ map { ( $_->[0] ) } @{$list->{ids}} ] }
861                 )->gather(1);
862
863                 for my $rec ( @$records ) {
864                         
865                         my $u = OpenILS::Utils::ModsParser->new();
866                         $u->start_mods_batch( $rec->marc );
867                         my $mods = $u->finish_mods_batch();
868
869                         push @{ $result->{records} }, { mvr => $mods, marcxml => $rec->marc, bibid => $rec->id };
870
871                 }
872
873         }
874
875     $cstore->disconnect();
876         return $result;
877 }
878
879 # ----------------------------------------------------------------------------
880 # These are the main OPAC search methods
881 # ----------------------------------------------------------------------------
882
883 __PACKAGE__->register_method(
884     method    => 'the_quest_for_knowledge',
885     api_name  => 'open-ils.search.biblio.multiclass',
886     signature => {
887         desc => "Performs a multi class biblio or metabib search",
888         params => [
889             {
890                 desc => "A search hash with keys: "
891                       . "searches, org_unit, depth, limit, offset, format, sort, sort_dir.  "
892                       . "See perldoc " . __PACKAGE__ . " for more detail",
893                 type => 'object',
894             },
895             {
896                 desc => "A flag to enable/disable searching and saving results in cache (default OFF)",
897                 type => 'string',
898             }
899         ],
900         return => {
901             desc => 'An object of the form: '
902                   . '{ "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }',
903         }
904     }
905 );
906
907 =head3 open-ils.search.biblio.multiclass (search-hash, docache)
908
909 The search-hash argument can have the following elements:
910
911     searches: { "$class" : "$value", ...}           [REQUIRED]
912     org_unit: The org id to focus the search at
913     depth   : The org depth     
914     limit   : The search limit      default: 10
915     offset  : The search offset     default:  0
916     format  : The MARC format
917     sort    : What field to sort the results on? [ author | title | pubdate ]
918     sort_dir: What direction do we sort? [ asc | desc ]
919     tag_circulated_records : Boolean, if true, records that are in the user's visible checkout history
920         will be tagged with an additional value ("1") as the last value in the record ID array for
921         each record.  Requires the 'authtoken'
922     authtoken : Authentication token string;  When actions are performed that require a user login
923         (e.g. tagging circulated records), the authentication token is required
924
925 The searches element is required, must have a hashref value, and the hashref must contain at least one 
926 of the following classes as a key:
927
928     title
929     author
930     subject
931     series
932     keyword
933
934 The value paired with a key is the associated search string.
935
936 The docache argument enables/disables searching and saving results in cache (default OFF).
937
938 The return object, if successful, will look like:
939
940     { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
941
942 =cut
943
944 __PACKAGE__->register_method(
945     method    => 'the_quest_for_knowledge',
946     api_name  => 'open-ils.search.biblio.multiclass.staff',
947     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
948 );
949 __PACKAGE__->register_method(
950     method    => 'the_quest_for_knowledge',
951     api_name  => 'open-ils.search.metabib.multiclass',
952     signature => q/@see open-ils.search.biblio.multiclass/
953 );
954 __PACKAGE__->register_method(
955     method    => 'the_quest_for_knowledge',
956     api_name  => 'open-ils.search.metabib.multiclass.staff',
957     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass/
958 );
959
960 sub the_quest_for_knowledge {
961         my( $self, $conn, $searchhash, $docache ) = @_;
962
963         return { count => 0 } unless $searchhash and
964                 ref $searchhash->{searches} eq 'HASH';
965
966         my $method = 'open-ils.storage.biblio.multiclass.search_fts';
967         my $ismeta = 0;
968         my @recs;
969
970         if($self->api_name =~ /metabib/) {
971                 $ismeta = 1;
972                 $method =~ s/biblio/metabib/o;
973         }
974
975         # do some simple sanity checking
976         if(!$searchhash->{searches} or
977                 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
978                 return { count => 0 };
979         }
980
981     my $offset = $searchhash->{offset} ||  0;   # user value or default in local var now
982     my $limit  = $searchhash->{limit}  || 10;   # user value or default in local var now
983     my $end    = $offset + $limit - 1;
984
985         my $maxlimit = 5000;
986     $searchhash->{offset} = 0;                  # possible user value overwritten in hash
987     $searchhash->{limit}  = $maxlimit;          # possible user value overwritten in hash
988
989         return { count => 0 } if $offset > $maxlimit;
990
991         my @search;
992         push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
993         my $s = OpenSRF::Utils::JSON->perl2JSON(\@search);
994         my $ckey = $pfx . md5_hex($method . $s);
995
996         $logger->info("bib search for: $s");
997
998         $searchhash->{limit} -= $offset;
999
1000
1001     my $trim = 0;
1002         my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
1003
1004         if(!$result) {
1005
1006                 $method .= ".staff" if($self->api_name =~ /staff/);
1007                 $method .= ".atomic";
1008         
1009                 for (keys %$searchhash) { 
1010                         delete $$searchhash{$_} 
1011                                 unless defined $$searchhash{$_}; 
1012                 }
1013         
1014                 $result = $U->storagereq( $method, %$searchhash );
1015         $trim = 1;
1016
1017         } else { 
1018                 $docache = 0;   # results came FROM cache, so we don't write back
1019         }
1020
1021         return {count => 0} unless ($result && $$result[0]);
1022
1023         @recs = @$result;
1024
1025         my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
1026
1027         if($docache) {
1028                 # If we didn't get this data from the cache, put it into the cache
1029                 # then return the correct offset of records
1030                 $logger->debug("putting search cache $ckey\n");
1031                 put_cache($ckey, $count, \@recs);
1032         }
1033
1034     if($trim) {
1035         # if we have the full set of data, trim out 
1036         # the requested chunk based on limit and offset
1037         my @t;
1038         for ($offset..$end) {
1039             last unless $recs[$_];
1040             push(@t, $recs[$_]);
1041         }
1042         @recs = @t;
1043     }
1044
1045         return { ids => \@recs, count => $count };
1046 }
1047
1048
1049 __PACKAGE__->register_method(
1050     method    => 'staged_search',
1051     api_name  => 'open-ils.search.biblio.multiclass.staged',
1052     signature => {
1053         desc   => 'Staged search filters out unavailable items.  This means that it relies on an estimation strategy for determining ' .
1054                   'how big a "raw" search result chunk (i.e. a "superpage") to obtain prior to filtering.  See "estimation_strategy" in your SRF config.',
1055         params => [
1056             {
1057                 desc => "A search hash with keys: "
1058                       . "searches, limit, offset.  The others are optional, but the 'searches' key/value pair is required, with the value being a hashref.  "
1059                       . "See perldoc " . __PACKAGE__ . " for more detail",
1060                 type => 'object',
1061             },
1062             {
1063                 desc => "A flag to enable/disable searching and saving results in cache, including facets (default OFF)",
1064                 type => 'string',
1065             }
1066         ],
1067         return => {
1068             desc => 'Hash with keys: count, core_limit, superpage_size, superpage_summary, facet_key, ids.  '
1069                   . 'The superpage_summary value is a hashref that includes keys: estimated_hit_count, visible.',
1070             type => 'object',
1071         }
1072     }
1073 );
1074 __PACKAGE__->register_method(
1075     method    => 'staged_search',
1076     api_name  => 'open-ils.search.biblio.multiclass.staged.staff',
1077     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1078 );
1079 __PACKAGE__->register_method(
1080     method    => 'staged_search',
1081     api_name  => 'open-ils.search.metabib.multiclass.staged',
1082     signature => q/@see open-ils.search.biblio.multiclass.staged/
1083 );
1084 __PACKAGE__->register_method(
1085     method    => 'staged_search',
1086     api_name  => 'open-ils.search.metabib.multiclass.staged.staff',
1087     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
1088 );
1089
1090 sub staged_search {
1091         my($self, $conn, $search_hash, $docache) = @_;
1092
1093     my $IAmMetabib = ($self->api_name =~ /metabib/) ? 1 : 0;
1094
1095     my $method = $IAmMetabib?
1096         'open-ils.storage.metabib.multiclass.staged.search_fts':
1097         'open-ils.storage.biblio.multiclass.staged.search_fts';
1098
1099     $method .= '.staff' if $self->api_name =~ /staff$/;
1100     $method .= '.atomic';
1101                 
1102     return {count => 0} unless (
1103         $search_hash and 
1104         $search_hash->{searches} and 
1105         scalar( keys %{$search_hash->{searches}} ));
1106
1107     my $search_duration;
1108     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
1109     my $user_limit  = $search_hash->{limit}  || 10;
1110     $user_offset = ($user_offset >= 0) ? $user_offset :  0;
1111     $user_limit  = ($user_limit  >= 0) ? $user_limit  : 10;
1112
1113
1114     # we're grabbing results on a per-superpage basis, which means the 
1115     # limit and offset should coincide with superpage boundaries
1116     $search_hash->{offset} = 0;
1117     $search_hash->{limit} = $superpage_size;
1118
1119     # force a well-known check_limit
1120     $search_hash->{check_limit} = $superpage_size; 
1121     # restrict total tested to superpage size * number of superpages
1122     $search_hash->{core_limit}  = $superpage_size * $max_superpages;
1123
1124     # Set the configured estimation strategy, defaults to 'inclusion'.
1125         my $estimation_strategy = OpenSRF::Utils::SettingsClient
1126         ->new
1127         ->config_value(
1128             apps => 'open-ils.search', app_settings => 'estimation_strategy'
1129         ) || 'inclusion';
1130         $search_hash->{estimation_strategy} = $estimation_strategy;
1131
1132     # pull any existing results from the cache
1133     my $key = search_cache_key($method, $search_hash);
1134     my $facet_key = $key.'_facets';
1135     my $cache_data = $cache->get_cache($key) || {};
1136
1137     # keep retrieving results until we find enough to 
1138     # fulfill the user-specified limit and offset
1139     my $all_results = [];
1140     my $page; # current superpage
1141     my $est_hit_count = 0;
1142     my $current_page_summary = {};
1143     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1144     my $is_real_hit_count = 0;
1145     my $new_ids = [];
1146
1147     for($page = 0; $page < $max_superpages; $page++) {
1148
1149         my $data = $cache_data->{$page};
1150         my $results;
1151         my $summary;
1152
1153         $logger->debug("staged search: analyzing superpage $page");
1154
1155         if($data) {
1156             # this window of results is already cached
1157             $logger->debug("staged search: found cached results");
1158             $summary = $data->{summary};
1159             $results = $data->{results};
1160
1161         } else {
1162             # retrieve the window of results from the database
1163             $logger->debug("staged search: fetching results from the database");
1164             $search_hash->{skip_check} = $page * $superpage_size;
1165             my $start = time;
1166             $results = $U->storagereq($method, %$search_hash);
1167             $search_duration = time - $start;
1168             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1169             $summary = shift(@$results) if $results;
1170
1171             unless($summary) {
1172                 $logger->info("search timed out: duration=$search_duration: params=".
1173                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1174                 return {count => 0};
1175             }
1176
1177             my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1178             if($hc == 0) {
1179                 $logger->info("search returned 0 results: duration=$search_duration: params=".
1180                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1181             }
1182
1183             # Create backwards-compatible result structures
1184             if($IAmMetabib) {
1185                 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1186             } else {
1187                 $results = [map {[$_->{id}]} @$results];
1188             }
1189
1190             tag_circulated_records($search_hash->{authtoken}, $results, $IAmMetabib) 
1191                 if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1192
1193             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1194             $results = [grep {defined $_->[0]} @$results];
1195             cache_staged_search_page($key, $page, $summary, $results) if $docache;
1196         }
1197
1198         $current_page_summary = $summary;
1199
1200         # add the new set of results to the set under construction
1201         push(@$all_results, @$results);
1202
1203         my $current_count = scalar(@$all_results);
1204
1205         $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1206             if $page == 0;
1207
1208         $logger->debug("staged search: located $current_count, with estimated hits=".
1209             $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1210
1211                 if (defined($summary->{estimated_hit_count})) {
1212             foreach (qw/ checked visible excluded deleted /) {
1213                 $global_summary->{$_} += $summary->{$_};
1214             }
1215                         $global_summary->{total} = $summary->{total};
1216                 }
1217
1218         # we've found all the possible hits
1219         last if $current_count == $summary->{visible}
1220             and not defined $summary->{estimated_hit_count};
1221
1222         # we've found enough results to satisfy the requested limit/offset
1223         last if $current_count >= ($user_limit + $user_offset);
1224
1225         # we've scanned all possible hits
1226         if($summary->{checked} < $superpage_size) {
1227             $est_hit_count = scalar(@$all_results);
1228             # we have all possible results in hand, so we know the final hit count
1229             $is_real_hit_count = 1;
1230             last;
1231         }
1232     }
1233
1234     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1235
1236         # refine the estimate if we have more than one superpage
1237         if ($page > 0 and not $is_real_hit_count) {
1238                 if ($global_summary->{checked} >= $global_summary->{total}) {
1239                         $est_hit_count = $global_summary->{visible};
1240                 } else {
1241                         my $updated_hit_count = $U->storagereq(
1242                                 'open-ils.storage.fts_paging_estimate',
1243                                 $global_summary->{checked},
1244                                 $global_summary->{visible},
1245                                 $global_summary->{excluded},
1246                                 $global_summary->{deleted},
1247                                 $global_summary->{total}
1248                         );
1249                         $est_hit_count = $updated_hit_count->{$estimation_strategy};
1250                 }
1251         }
1252
1253     $conn->respond_complete(
1254         {
1255             count             => $est_hit_count,
1256             core_limit        => $search_hash->{core_limit},
1257             superpage_size    => $search_hash->{check_limit},
1258             superpage_summary => $current_page_summary,
1259             facet_key         => $facet_key,
1260             ids               => \@results
1261         }
1262     );
1263
1264     cache_facets($facet_key, $new_ids, $IAmMetabib) if $docache;
1265
1266     return undef;
1267 }
1268
1269 sub tag_circulated_records {
1270     my ($auth, $results, $metabib) = @_;
1271     my $e = new_editor(authtoken => $auth);
1272     return $results unless $e->checkauth;
1273
1274     my $query = {
1275         select   => { acn => [{ column => 'record', alias => 'tagme' }] }, 
1276         from     => { acp => 'acn' }, 
1277         where    => { id => { in => { from => ['action.usr_visible_circ_copies', $e->requestor->id] } } },
1278         distinct => 1
1279     };
1280
1281     if ($metabib) {
1282         $query = {
1283             select   => { mmsm => [{ column => 'metarecord', alias => 'tagme' }] },
1284             from     => 'mmsm',
1285             where    => { source => { in => $query } },
1286             distinct => 1
1287         };
1288     }
1289
1290     # Give me the distinct set of bib records that exist in the user's visible circulation history
1291     my $circ_recs = $e->json_query( $query );
1292
1293     # if the record appears in the circ history, push a 1 onto 
1294     # the rec array structure to indicate truthiness
1295     for my $rec (@$results) {
1296         push(@$rec, 1) if grep { $_->{tagme} eq $$rec[0] } @$circ_recs;
1297     }
1298
1299     $results
1300 }
1301
1302 # creates a unique token to represent the query in the cache
1303 sub search_cache_key {
1304     my $method = shift;
1305     my $search_hash = shift;
1306         my @sorted;
1307     for my $key (sort keys %$search_hash) {
1308             push(@sorted, ($key => $$search_hash{$key})) 
1309             unless $key eq 'limit'  or 
1310                    $key eq 'offset' or 
1311                    $key eq 'skip_check';
1312     }
1313         my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1314         return $pfx . md5_hex($method . $s);
1315 }
1316
1317 sub retrieve_cached_facets {
1318     my $self   = shift;
1319     my $client = shift;
1320     my $key    = shift;
1321     my $limit    = shift;
1322
1323     return undef unless ($key and $key =~ /_facets$/);
1324
1325     my $blob = $cache->get_cache($key) || {};
1326
1327     my $facets = {};
1328     if ($limit) {
1329        for my $f ( keys %$blob ) {
1330             my @sorted = map{ { $$_[1] => $$_[0] } } sort {$$b[0] <=> $$a[0] || $$a[1] cmp $$b[1]} map { [$$blob{$f}{$_}, $_] } keys %{ $$blob{$f} };
1331             @sorted = @sorted[0 .. $limit - 1] if (scalar(@sorted) > $limit);
1332             for my $s ( @sorted ) {
1333                 my ($k) = keys(%$s);
1334                 my ($v) = values(%$s);
1335                 $$facets{$f}{$k} = $v;
1336             }
1337         }
1338     } else {
1339         $facets = $blob;
1340     }
1341
1342     return $facets;
1343 }
1344
1345 __PACKAGE__->register_method(
1346     method   => "retrieve_cached_facets",
1347     api_name => "open-ils.search.facet_cache.retrieve",
1348     signature => {
1349         desc   => 'Returns facet data derived from a specific search based on a key '.
1350                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1351         params => [
1352             {
1353                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1354                 type => 'string',
1355             }
1356         ],
1357         return => {
1358             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1359                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1360                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1361                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1362             type => 'object',
1363         }
1364     }
1365 );
1366
1367
1368 sub cache_facets {
1369     # add facets for this search to the facet cache
1370     my($key, $results, $metabib) = @_;
1371     my $data = $cache->get_cache($key);
1372     $data ||= {};
1373
1374     return undef unless (@$results);
1375
1376     # The query we're constructing
1377     #
1378     # select  mfae.field as id,
1379     #         mfae.value,
1380     #         count(distinct mmrsm.appropriate-id-field )
1381     #   from  metabib.facet_entry mfae
1382     #         join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1383     #   where mmrsm.appropriate-id-field in IDLIST
1384     #   group by 1,2;
1385
1386     my $count_field = $metabib ? 'metarecord' : 'source';
1387     my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1388         {   select  => {
1389                 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1390                 mmrsm => [{
1391                     transform => 'count',
1392                     distinct => 1,
1393                     column => $count_field,
1394                     alias => 'count',
1395                     aggregate => 1
1396                 }]
1397             },
1398             from    => {
1399                 mfae => {
1400                     mmrsm => { field => 'source', fkey => 'source' }
1401                 }
1402             },
1403             where   => {
1404                 '+mmrsm' => { $count_field => $results }
1405             }
1406         }
1407     );
1408
1409     for my $facet (@$facets) {
1410         next unless ($facet->{value});
1411         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1412     }
1413
1414     $logger->info("facet compilation: cached with key=$key");
1415
1416     $cache->put_cache($key, $data, $cache_timeout);
1417 }
1418
1419 sub cache_staged_search_page {
1420     # puts this set of results into the cache
1421     my($key, $page, $summary, $results) = @_;
1422     my $data = $cache->get_cache($key);
1423     $data ||= {};
1424     $data->{$page} = {
1425         summary => $summary,
1426         results => $results
1427     };
1428
1429     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1430         $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1431
1432     $cache->put_cache($key, $data, $cache_timeout);
1433 }
1434
1435 sub search_cache {
1436
1437         my $key         = shift;
1438         my $offset      = shift;
1439         my $limit       = shift;
1440         my $start       = $offset;
1441         my $end         = $offset + $limit - 1;
1442
1443         $logger->debug("searching cache for $key : $start..$end\n");
1444
1445         return undef unless $cache;
1446         my $data = $cache->get_cache($key);
1447
1448         return undef unless $data;
1449
1450         my $count = $data->[0];
1451         $data = $data->[1];
1452
1453         return undef unless $offset < $count;
1454
1455         my @result;
1456         for( my $i = $offset; $i <= $end; $i++ ) {
1457                 last unless my $d = $$data[$i];
1458                 push( @result, $d );
1459         }
1460
1461         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1462
1463         return \@result;
1464 }
1465
1466
1467 sub put_cache {
1468         my( $key, $count, $data ) = @_;
1469         return undef unless $cache;
1470         $logger->debug("search_cache putting ".
1471                 scalar(@$data)." items at key $key with timeout $cache_timeout");
1472         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1473 }
1474
1475
1476 __PACKAGE__->register_method(
1477     method   => "biblio_mrid_to_modsbatch_batch",
1478     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1479 );
1480
1481 sub biblio_mrid_to_modsbatch_batch {
1482         my( $self, $client, $mrids) = @_;
1483         # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1484         my @mods;
1485         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1486         for my $id (@$mrids) {
1487                 next unless defined $id;
1488                 my ($m) = $method->run($id);
1489                 push @mods, $m;
1490         }
1491         return \@mods;
1492 }
1493
1494
1495 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1496              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1497     {
1498     __PACKAGE__->register_method(
1499         method    => "biblio_mrid_to_modsbatch",
1500         api_name  => $_,
1501         signature => {
1502             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1503                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1504             params => [
1505                 { desc => 'Metarecord ID', type => 'number' },
1506                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1507             ],
1508             return => {
1509                 desc => 'MVR Object, event on error',
1510             }
1511         }
1512     );
1513 }
1514
1515 sub biblio_mrid_to_modsbatch {
1516         my( $self, $client, $mrid, $args) = @_;
1517
1518         # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1519
1520         my ($mr, $evt) = _grab_metarecord($mrid);
1521         return $evt unless $mr;
1522
1523         my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1524               biblio_mrid_make_modsbatch($self, $client, $mr);
1525
1526         return $mvr unless ref($args);  
1527
1528         # Here we find the lead record appropriate for the given filters 
1529         # and use that for the title and author of the metarecord
1530     my $format = $$args{format};
1531     my $org    = $$args{org};
1532     my $depth  = $$args{depth};
1533
1534         return $mvr unless $format or $org or $depth;
1535
1536         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1537         $method = "$method.staff" if $self->api_name =~ /staff/o; 
1538
1539         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1540
1541         if( my $mods = $U->record_to_mvr($rec) ) {
1542
1543         $mvr->title( $mods->title );
1544         $mvr->author($mods->author);
1545                 $logger->debug("mods_slim updating title and ".
1546                         "author in mvr with ".$mods->title." : ".$mods->author);
1547         }
1548
1549         return $mvr;
1550 }
1551
1552 # converts a metarecord to an mvr
1553 sub _mr_to_mvr {
1554         my $mr = shift;
1555         my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1556         return Fieldmapper::metabib::virtual_record->new($perl);
1557 }
1558
1559 # checks to see if a metarecord has mods, if so returns true;
1560
1561 __PACKAGE__->register_method(
1562     method   => "biblio_mrid_check_mvr",
1563     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1564     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1565               . "if the metarecord already has an mvr associated with it."
1566 );
1567
1568 sub biblio_mrid_check_mvr {
1569         my( $self, $client, $mrid ) = @_;
1570         my $mr; 
1571
1572         my $evt;
1573         if(ref($mrid)) { $mr = $mrid; } 
1574         else { ($mr, $evt) = _grab_metarecord($mrid); }
1575         return $evt if $evt;
1576
1577         # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1578
1579         return _mr_to_mvr($mr) if $mr->mods();
1580         return undef;
1581 }
1582
1583 sub _grab_metarecord {
1584         my $mrid = shift;
1585         #my $e = OpenILS::Utils::Editor->new;
1586         my $e = new_editor();
1587         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1588         return ($mr);
1589 }
1590
1591
1592 __PACKAGE__->register_method(
1593     method   => "biblio_mrid_make_modsbatch",
1594     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1595     notes    => "Takes either a metarecord ID or a metarecord object. "
1596               . "Forces the creations of an mvr for the given metarecord. "
1597               . "The created mvr is returned."
1598 );
1599
1600 sub biblio_mrid_make_modsbatch {
1601         my( $self, $client, $mrid ) = @_;
1602
1603         #my $e = OpenILS::Utils::Editor->new;
1604         my $e = new_editor();
1605
1606         my $mr;
1607         if( ref($mrid) ) {
1608                 $mr = $mrid;
1609                 $mrid = $mr->id;
1610         } else {
1611                 $mr = $e->retrieve_metabib_metarecord($mrid) 
1612                         or return $e->event;
1613         }
1614
1615         my $masterid = $mr->master_record;
1616         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1617
1618         my $ids = $U->storagereq(
1619                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1620         return undef unless @$ids;
1621
1622         my $master = $e->retrieve_biblio_record_entry($masterid)
1623                 or return $e->event;
1624
1625         # start the mods batch
1626         my $u = OpenILS::Utils::ModsParser->new();
1627         $u->start_mods_batch( $master->marc );
1628
1629         # grab all of the sub-records and shove them into the batch
1630         my @ids = grep { $_ ne $masterid } @$ids;
1631         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1632
1633         my $subrecs = [];
1634         if(@$ids) {
1635                 for my $i (@$ids) {
1636                         my $r = $e->retrieve_biblio_record_entry($i);
1637                         push( @$subrecs, $r ) if $r;
1638                 }
1639         }
1640
1641         for(@$subrecs) {
1642                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1643                 $u->push_mods_batch( $_->marc ) if $_->marc;
1644         }
1645
1646
1647         # finish up and send to the client
1648         my $mods = $u->finish_mods_batch();
1649         $mods->doc_id($mrid);
1650         $client->respond_complete($mods);
1651
1652
1653         # now update the mods string in the db
1654         my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1655         $mr->mods($string);
1656
1657         #$e = OpenILS::Utils::Editor->new(xact => 1);
1658         $e = new_editor(xact => 1);
1659         $e->update_metabib_metarecord($mr) 
1660                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1661         $e->finish;
1662
1663         return undef;
1664 }
1665
1666
1667 # converts a mr id into a list of record ids
1668
1669 foreach (qw/open-ils.search.biblio.metarecord_to_records
1670             open-ils.search.biblio.metarecord_to_records.staff/)
1671 {
1672     __PACKAGE__->register_method(
1673         method    => "biblio_mrid_to_record_ids",
1674         api_name  => $_,
1675         signature => {
1676             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1677                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1678             params => [
1679                 { desc => 'Metarecord ID', type => 'number' },
1680                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1681             ],
1682             return => {
1683                 desc => 'Results object like {count => $i, ids =>[...]}',
1684                 type => 'object'
1685             }
1686             
1687         }
1688     );
1689 }
1690
1691 sub biblio_mrid_to_record_ids {
1692         my( $self, $client, $mrid, $args ) = @_;
1693
1694     my $format = $$args{format};
1695     my $org    = $$args{org};
1696     my $depth  = $$args{depth};
1697
1698         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1699         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1700         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1701
1702         return { count => scalar(@$recs), ids => $recs };
1703 }
1704
1705
1706 __PACKAGE__->register_method(
1707     method   => "biblio_record_to_marc_html",
1708     api_name => "open-ils.search.biblio.record.html"
1709 );
1710
1711 __PACKAGE__->register_method(
1712     method   => "biblio_record_to_marc_html",
1713     api_name => "open-ils.search.authority.to_html"
1714 );
1715
1716 # Persistent parsers and setting objects
1717 my $parser = XML::LibXML->new();
1718 my $xslt   = XML::LibXSLT->new();
1719 my $marc_sheet;
1720 my $slim_marc_sheet;
1721 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1722
1723 sub biblio_record_to_marc_html {
1724         my($self, $client, $recordid, $slim, $marcxml) = @_;
1725
1726     my $sheet;
1727         my $dir = $settings_client->config_value("dirs", "xsl");
1728
1729     if($slim) {
1730         unless($slim_marc_sheet) {
1731                     my $xsl = $settings_client->config_value(
1732                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1733             if($xsl) {
1734                         $xsl = $parser->parse_file("$dir/$xsl");
1735                         $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1736             }
1737         }
1738         $sheet = $slim_marc_sheet;
1739     }
1740
1741     unless($sheet) {
1742         unless($marc_sheet) {
1743             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1744                     my $xsl = $settings_client->config_value(
1745                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1746                     $xsl = $parser->parse_file("$dir/$xsl");
1747                     $marc_sheet = $xslt->parse_stylesheet($xsl);
1748         }
1749         $sheet = $marc_sheet;
1750     }
1751
1752     my $record;
1753     unless($marcxml) {
1754         my $e = new_editor();
1755         if($self->api_name =~ /authority/) {
1756             $record = $e->retrieve_authority_record_entry($recordid)
1757                 or return $e->event;
1758         } else {
1759             $record = $e->retrieve_biblio_record_entry($recordid)
1760                 or return $e->event;
1761         }
1762         $marcxml = $record->marc;
1763     }
1764
1765         my $xmldoc = $parser->parse_string($marcxml);
1766         my $html = $sheet->transform($xmldoc);
1767         return $html->documentElement->toString();
1768 }
1769
1770 __PACKAGE__->register_method(
1771     method    => "format_biblio_record_entry",
1772     api_name  => "open-ils.search.biblio.record.print",
1773     signature => {
1774         desc   => 'Returns a printable version of the specified bib record',
1775         params => [
1776             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1777         ],
1778         return => {
1779             desc => q/An action_trigger.event object or error event./,
1780             type => 'object',
1781         }
1782     }
1783 );
1784 __PACKAGE__->register_method(
1785     method    => "format_biblio_record_entry",
1786     api_name  => "open-ils.search.biblio.record.email",
1787     signature => {
1788         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1789         params => [
1790             { desc => 'Authentication token',  type => 'string'},
1791             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1792         ],
1793         return => {
1794             desc => q/Undefined on success, otherwise an error event./,
1795             type => 'object',
1796         }
1797     }
1798 );
1799
1800 sub format_biblio_record_entry {
1801     my($self, $conn, $arg1, $arg2) = @_;
1802
1803     my $for_print = ($self->api_name =~ /print/);
1804     my $for_email = ($self->api_name =~ /email/);
1805
1806     my $e; my $auth; my $bib_id; my $context_org;
1807
1808     if ($for_print) {
1809         $bib_id = $arg1;
1810         $context_org = $arg2 || $U->fetch_org_tree->id;
1811         $e = new_editor(xact => 1);
1812     } elsif ($for_email) {
1813         $auth = $arg1;
1814         $bib_id = $arg2;
1815         $e = new_editor(authtoken => $auth, xact => 1);
1816         return $e->die_event unless $e->checkauth;
1817         $context_org = $e->requestor->home_ou;
1818     }
1819
1820     my $bib_ids;
1821     if (ref $bib_id ne 'ARRAY') {
1822         $bib_ids = [ $bib_id ];
1823     } else {
1824         $bib_ids = $bib_id;
1825     }
1826
1827     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1828     $bucket->btype('temp');
1829     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1830     if ($for_email) {
1831         $bucket->owner($e->requestor) 
1832     } else {
1833         $bucket->owner(1);
1834     }
1835     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1836
1837     for my $id (@$bib_ids) {
1838
1839         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1840
1841         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1842         $bucket_entry->target_biblio_record_entry($bib);
1843         $bucket_entry->bucket($bucket_obj->id);
1844         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1845     }
1846
1847     $e->commit;
1848
1849     if ($for_print) {
1850
1851         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1852
1853     } elsif ($for_email) {
1854
1855         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1856     }
1857
1858     return undef;
1859 }
1860
1861
1862 __PACKAGE__->register_method(
1863     method   => "retrieve_all_copy_statuses",
1864     api_name => "open-ils.search.config.copy_status.retrieve.all"
1865 );
1866
1867 sub retrieve_all_copy_statuses {
1868         my( $self, $client ) = @_;
1869         return new_editor()->retrieve_all_config_copy_status();
1870 }
1871
1872
1873 __PACKAGE__->register_method(
1874     method   => "copy_counts_per_org",
1875     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1876 );
1877
1878 __PACKAGE__->register_method(
1879     method   => "copy_counts_per_org",
1880     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1881 );
1882
1883 sub copy_counts_per_org {
1884         my( $self, $client, $record_id ) = @_;
1885
1886         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1887
1888         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1889         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1890
1891         my $counts = $apputils->simple_scalar_request(
1892                 "open-ils.storage", $method, $record_id );
1893
1894         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1895         return $counts;
1896 }
1897
1898
1899 __PACKAGE__->register_method(
1900     method   => "copy_count_summary",
1901     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1902     notes    => "returns an array of these: "
1903               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1904               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1905 );
1906                 
1907
1908 sub copy_count_summary {
1909         my( $self, $client, $rid, $org, $depth ) = @_;
1910     $org   ||= 1;
1911     $depth ||= 0;
1912     my $data = $U->storagereq(
1913                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1914
1915     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1916 }
1917
1918 __PACKAGE__->register_method(
1919     method   => "copy_location_count_summary",
1920     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1921     notes    => "returns an array of these: "
1922               . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1923               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1924 );
1925
1926 sub copy_location_count_summary {
1927     my( $self, $client, $rid, $org, $depth ) = @_;
1928     $org   ||= 1;
1929     $depth ||= 0;
1930     my $data = $U->storagereq(
1931                 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1932
1933     return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1934 }
1935
1936 __PACKAGE__->register_method(
1937     method   => "copy_count_location_summary",
1938     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1939     notes    => "returns an array of these: "
1940               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1941               . "where statusx is a copy status name.  The statuses are sorted by ID."
1942 );
1943
1944 sub copy_count_location_summary {
1945     my( $self, $client, $rid, $org, $depth ) = @_;
1946     $org   ||= 1;
1947     $depth ||= 0;
1948     my $data = $U->storagereq(
1949         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1950     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1951 }
1952
1953
1954 foreach (qw/open-ils.search.biblio.marc
1955             open-ils.search.biblio.marc.staff/)
1956 {
1957 __PACKAGE__->register_method(
1958     method    => "marc_search",
1959     api_name  => $_,
1960     signature => {
1961         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
1962                 . 'As usual, the .staff version of the search includes otherwise hidden records',
1963         params => [
1964             {
1965                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1966                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
1967                 type => 'object'
1968             },
1969             {desc => 'limit (optional)',  type => 'number'},
1970             {desc => 'offset (optional)', type => 'number'}
1971         ],
1972         return => {
1973             desc => 'Results object like: { "count": $i, "ids": [...] }',
1974             type => 'object'
1975         }
1976     }
1977 );
1978 }
1979
1980 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1981
1982 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
1983
1984     searches: complex query object  (required)
1985     org_unit: The org ID to focus the search at
1986     depth   : The org depth     
1987     limit   : integer search limit      default: 10
1988     offset  : integer search offset     default:  0
1989     sort    : What field to sort the results on? [ author | title | pubdate ]
1990     sort_dir: In what direction do we sort? [ asc | desc ]
1991
1992 Additional keys to refine search criteria:
1993
1994     audience : Audience
1995     language : Language (code)
1996     lit_form : Literary form
1997     item_form: Item form
1998     item_type: Item type
1999     format   : The MARC format
2000
2001 Please note that the specific strings to be used in the "addtional keys" will be entirely
2002 dependent on your loaded data.  
2003
2004 All keys except "searches" are optional.
2005 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
2006
2007 For example, an arg hash might look like:
2008
2009     $arghash = {
2010         searches => [
2011             {
2012                 term     => "harry",
2013                 restrict => [
2014                     {
2015                         tag => 245,
2016                         subfield => "a"
2017                     }
2018                     # ...
2019                 ]
2020             }
2021             # ...
2022         ],
2023         org_unit  => 1,
2024         limit     => 5,
2025         sort      => "author",
2026         item_type => "g"
2027     }
2028
2029 The arghash is eventually passed to the SRF call:
2030 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2031
2032 Presently, search uses the cache unconditionally.
2033
2034 =cut
2035
2036 # FIXME: that example above isn't actually tested.
2037 # TODO: docache option?
2038 sub marc_search {
2039         my( $self, $conn, $args, $limit, $offset ) = @_;
2040
2041         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2042         $method .= ".staff" if $self->api_name =~ /staff/;
2043         $method .= ".atomic";
2044
2045     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
2046     $offset ||=  0;     # FIXME: what about $args->{offset} ?
2047
2048         my @search;
2049         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2050         my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2051
2052         my $recs = search_cache($ckey, $offset, $limit);
2053
2054         if(!$recs) {
2055                 $recs = $U->storagereq($method, %$args) || [];
2056                 if( $recs ) {
2057                         put_cache($ckey, scalar(@$recs), $recs);
2058                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2059                 } else {
2060                         $recs = [];
2061                 }
2062         }
2063
2064         my $count = 0;
2065         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2066         my @recs = map { $_->[0] } @$recs;
2067
2068         return { ids => \@recs, count => $count };
2069 }
2070
2071
2072 __PACKAGE__->register_method(
2073     method    => "biblio_search_isbn",
2074     api_name  => "open-ils.search.biblio.isbn",
2075     signature => {
2076         desc   => 'Retrieve biblio IDs for a given ISBN',
2077         params => [
2078             {desc => 'ISBN', type => 'string'}  # or number maybe?  How normalized is our storage data?
2079         ],
2080         return => {
2081             desc => 'Results object like: { "count": $i, "ids": [...] }',
2082             type => 'object'
2083         }
2084     }
2085 );
2086
2087 sub biblio_search_isbn { 
2088         my( $self, $client, $isbn ) = @_;
2089         $logger->debug("Searching ISBN $isbn");
2090         my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2091         return { ids => $recs, count => scalar(@$recs) };
2092 }
2093
2094 __PACKAGE__->register_method(
2095     method   => "biblio_search_isbn_batch",
2096     api_name => "open-ils.search.biblio.isbn_list",
2097 );
2098
2099 sub biblio_search_isbn_batch { 
2100         my( $self, $client, $isbn_list ) = @_;
2101         $logger->debug("Searching ISBNs @$isbn_list");
2102         my @recs = (); my %rec_set = ();
2103         foreach my $isbn ( @$isbn_list ) {
2104                 foreach my $rec ( @{ $U->storagereq(
2105                         'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2106                 } ) {
2107                         if (! $rec_set{ $rec }) {
2108                                 $rec_set{ $rec } = 1;
2109                                 push @recs, $rec;
2110                         }
2111                 }
2112         }
2113         return { ids => \@recs, count => scalar(@recs) };
2114 }
2115
2116 __PACKAGE__->register_method(
2117     method   => "biblio_search_issn",
2118     api_name => "open-ils.search.biblio.issn",
2119     signature => {
2120         desc   => 'Retrieve biblio IDs for a given ISSN',
2121         params => [
2122             {desc => 'ISBN', type => 'string'}
2123         ],
2124         return => {
2125             desc => 'Results object like: { "count": $i, "ids": [...] }',
2126             type => 'object'
2127         }
2128     }
2129 );
2130
2131 sub biblio_search_issn { 
2132         my( $self, $client, $issn ) = @_;
2133         $logger->debug("Searching ISSN $issn");
2134         my $e = new_editor();
2135         $issn =~ s/-/ /g;
2136         my $recs = $U->storagereq(
2137                 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2138         return { ids => $recs, count => scalar(@$recs) };
2139 }
2140
2141
2142 __PACKAGE__->register_method(
2143     method    => "fetch_mods_by_copy",
2144     api_name  => "open-ils.search.biblio.mods_from_copy",
2145     argc      => 1,
2146     signature => {
2147         desc    => 'Retrieve MODS record given an attached copy ID',
2148         params  => [
2149             { desc => 'Copy ID', type => 'number' }
2150         ],
2151         returns => {
2152             desc => 'MODS record, event on error or uncataloged item'
2153         }
2154     }
2155 );
2156
2157 sub fetch_mods_by_copy {
2158         my( $self, $client, $copyid ) = @_;
2159         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2160         return $evt if $evt;
2161         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2162         return $apputils->record_to_mvr($record);
2163 }
2164
2165
2166 # -------------------------------------------------------------------------------------
2167
2168 __PACKAGE__->register_method(
2169     method   => "cn_browse",
2170     api_name => "open-ils.search.callnumber.browse.target",
2171     notes    => "Starts a callnumber browse"
2172 );
2173
2174 __PACKAGE__->register_method(
2175     method   => "cn_browse",
2176     api_name => "open-ils.search.callnumber.browse.page_up",
2177     notes    => "Returns the previous page of callnumbers",
2178 );
2179
2180 __PACKAGE__->register_method(
2181     method   => "cn_browse",
2182     api_name => "open-ils.search.callnumber.browse.page_down",
2183     notes    => "Returns the next page of callnumbers",
2184 );
2185
2186
2187 # RETURNS array of arrays like so: label, owning_lib, record, id
2188 sub cn_browse {
2189         my( $self, $client, @params ) = @_;
2190         my $method;
2191
2192         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2193                 if( $self->api_name =~ /target/ );
2194         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2195                 if( $self->api_name =~ /page_up/ );
2196         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2197                 if( $self->api_name =~ /page_down/ );
2198
2199         return $apputils->simplereq( 'open-ils.storage', $method, @params );
2200 }
2201 # -------------------------------------------------------------------------------------
2202
2203 __PACKAGE__->register_method(
2204     method        => "fetch_cn",
2205     api_name      => "open-ils.search.callnumber.retrieve",
2206     authoritative => 1,
2207     notes         => "retrieves a callnumber based on ID",
2208 );
2209
2210 sub fetch_cn {
2211         my( $self, $client, $id ) = @_;
2212         my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2213         return $evt if $evt;
2214         return $cn;
2215 }
2216
2217 __PACKAGE__->register_method(
2218     method    => "fetch_copy_by_cn",
2219     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2220     signature => q/
2221                 Returns an array of copy ID's by callnumber ID
2222                 @param cnid The callnumber ID
2223                 @return An array of copy IDs
2224         /
2225 );
2226
2227 sub fetch_copy_by_cn {
2228         my( $self, $conn, $cnid ) = @_;
2229         return $U->cstorereq(
2230                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2231                 { call_number => $cnid, deleted => 'f' } );
2232 }
2233
2234 __PACKAGE__->register_method(
2235     method    => 'fetch_cn_by_info',
2236     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2237     signature => q/
2238                 @param label The callnumber label
2239                 @param record The record the cn is attached to
2240                 @param org The owning library of the cn
2241                 @return The callnumber object
2242         /
2243 );
2244
2245
2246 sub fetch_cn_by_info {
2247         my( $self, $conn, $label, $record, $org ) = @_;
2248         return $U->cstorereq(
2249                 'open-ils.cstore.direct.asset.call_number.search',
2250                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2251 }
2252
2253
2254
2255 __PACKAGE__->register_method(
2256     method   => 'bib_extras',
2257     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2258 );
2259 __PACKAGE__->register_method(
2260     method   => 'bib_extras',
2261     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2262 );
2263 __PACKAGE__->register_method(
2264     method   => 'bib_extras',
2265     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2266 );
2267 __PACKAGE__->register_method(
2268     method   => 'bib_extras',
2269     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2270 );
2271 __PACKAGE__->register_method(
2272     method   => 'bib_extras',
2273     api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2274 );
2275
2276 sub bib_extras {
2277         my $self = shift;
2278
2279         my $e = new_editor();
2280
2281         return $e->retrieve_all_config_lit_form_map()
2282                 if( $self->api_name =~ /lit_form/ );
2283
2284         return $e->retrieve_all_config_item_form_map()
2285                 if( $self->api_name =~ /item_form_map/ );
2286
2287         return $e->retrieve_all_config_item_type_map()
2288                 if( $self->api_name =~ /item_type_map/ );
2289
2290         return $e->retrieve_all_config_bib_level_map()
2291                 if( $self->api_name =~ /bib_level_map/ );
2292
2293         return $e->retrieve_all_config_audience_map()
2294                 if( $self->api_name =~ /audience_map/ );
2295
2296         return [];
2297 }
2298
2299
2300
2301 __PACKAGE__->register_method(
2302     method    => 'fetch_slim_record',
2303     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2304     signature => {
2305         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2306         params => [
2307             { desc => 'Array of Record IDs', type => 'array' }
2308         ],
2309         return => { 
2310             desc => 'Array of biblio records, event on error'
2311         }
2312     }
2313 );
2314
2315 sub fetch_slim_record {
2316     my( $self, $conn, $ids ) = @_;
2317
2318 #my $editor = OpenILS::Utils::Editor->new;
2319     my $editor = new_editor();
2320         my @res;
2321     for( @$ids ) {
2322         return $editor->event unless
2323             my $r = $editor->retrieve_biblio_record_entry($_);
2324         $r->clear_marc;
2325         push(@res, $r);
2326     }
2327     return \@res;
2328 }
2329
2330
2331
2332 __PACKAGE__->register_method(
2333     method    => 'rec_to_mr_rec_descriptors',
2334     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2335     signature => q/
2336                 specialized method...
2337                 Given a biblio record id or a metarecord id, 
2338                 this returns a list of metabib.record_descriptor
2339                 objects that live within the same metarecord
2340                 @param args Object of args including:
2341         /
2342 );
2343
2344 sub rec_to_mr_rec_descriptors {
2345         my( $self, $conn, $args ) = @_;
2346
2347     my $rec        = $$args{record};
2348     my $mrec       = $$args{metarecord};
2349     my $item_forms = $$args{item_forms};
2350     my $item_types = $$args{item_types};
2351     my $item_lang  = $$args{item_lang};
2352
2353         my $e = new_editor();
2354         my $recs;
2355
2356         if( !$mrec ) {
2357                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2358                 return $e->event unless @$map;
2359                 $mrec = $$map[0]->metarecord;
2360         }
2361
2362         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2363         return $e->event unless @$recs;
2364
2365         my @recs = map { $_->source } @$recs;
2366         my $search = { record => \@recs };
2367         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2368         $search->{item_type} = $item_types if $item_types and @$item_types;
2369         $search->{item_lang} = $item_lang  if $item_lang;
2370
2371         my $desc = $e->search_metabib_record_descriptor($search);
2372
2373         return { metarecord => $mrec, descriptors => $desc };
2374 }
2375
2376
2377 __PACKAGE__->register_method(
2378     method   => 'fetch_age_protect',
2379     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2380 );
2381
2382 sub fetch_age_protect {
2383         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2384 }
2385
2386
2387 __PACKAGE__->register_method(
2388     method   => 'copies_by_cn_label',
2389     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2390 );
2391
2392 __PACKAGE__->register_method(
2393     method   => 'copies_by_cn_label',
2394     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2395 );
2396
2397 sub copies_by_cn_label {
2398         my( $self, $conn, $record, $label, $circ_lib ) = @_;
2399         my $e = new_editor();
2400         my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2401         return [] unless @$cns;
2402
2403         # show all non-deleted copies in the staff client ...
2404         if ($self->api_name =~ /staff$/o) {
2405                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2406         }
2407
2408         # ... otherwise, grab the copies ...
2409         my $copies = $e->search_asset_copy(
2410                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2411                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2412                 ]
2413         );
2414
2415         # ... and test for location and status visibility
2416         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2417 }
2418
2419
2420 1;
2421