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