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