]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Search/Biblio.pm
display copy counts correctly in staff search results
[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
1320     return undef unless ($key and $key =~ /_facets$/);
1321
1322     return $cache->get_cache($key) || {};
1323 }
1324
1325 __PACKAGE__->register_method(
1326     method   => "retrieve_cached_facets",
1327     api_name => "open-ils.search.facet_cache.retrieve",
1328     signature => {
1329         desc   => 'Returns facet data derived from a specific search based on a key '.
1330                   'generated by open-ils.search.biblio.multiclass.staged and friends.',
1331         params => [
1332             {
1333                 desc => "The facet cache key returned with the initial search as the facet_key hash value",
1334                 type => 'string',
1335             }
1336         ],
1337         return => {
1338             desc => 'Two level hash of facet values.  Top level key is the facet id defined on the config.metabib_field table.  '.
1339                     'Second level key is a string facet value.  Datum attached to each facet value is the number of distinct records, '.
1340                     'or metarecords for a metarecord search, which use that facet value and are visible to the search at the time of '.
1341                     'facet retrieval.  These counts are calculated for all superpages that have been checked for visibility.',
1342             type => 'object',
1343         }
1344     }
1345 );
1346
1347
1348 sub cache_facets {
1349     # add facets for this search to the facet cache
1350     my($key, $results, $metabib) = @_;
1351     my $data = $cache->get_cache($key);
1352     $data ||= {};
1353
1354     return undef unless (@$results);
1355
1356     # The query we're constructing
1357     #
1358     # select  mfae.field as id,
1359     #         mfae.value,
1360     #         count(distinct mmrsm.appropriate-id-field )
1361     #   from  metabib.facet_entry mfae
1362     #         join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1363     #   where mmrsm.appropriate-id-field in IDLIST
1364     #   group by 1,2;
1365
1366     my $count_field = $metabib ? 'metarecord' : 'source';
1367     my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1368         {   select  => {
1369                 mfae => [ { column => 'field', alias => 'id'}, 'value' ],
1370                 mmrsm => [{
1371                     transform => 'count',
1372                     distinct => 1,
1373                     column => $count_field,
1374                     alias => 'count',
1375                     aggregate => 1
1376                 }]
1377             },
1378             from    => {
1379                 mfae => {
1380                     mmrsm => { field => 'source', fkey => 'source' }
1381                 }
1382             },
1383             where   => {
1384                 '+mmrsm' => { $count_field => $results }
1385             }
1386         }
1387     );
1388
1389     for my $facet (@$facets) {
1390         next unless ($facet->{value});
1391         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1392     }
1393
1394     $logger->info("facet compilation: cached with key=$key");
1395
1396     $cache->put_cache($key, $data, $cache_timeout);
1397 }
1398
1399 sub cache_staged_search_page {
1400     # puts this set of results into the cache
1401     my($key, $page, $summary, $results) = @_;
1402     my $data = $cache->get_cache($key);
1403     $data ||= {};
1404     $data->{$page} = {
1405         summary => $summary,
1406         results => $results
1407     };
1408
1409     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1410         $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1411
1412     $cache->put_cache($key, $data, $cache_timeout);
1413 }
1414
1415 sub search_cache {
1416
1417         my $key         = shift;
1418         my $offset      = shift;
1419         my $limit       = shift;
1420         my $start       = $offset;
1421         my $end         = $offset + $limit - 1;
1422
1423         $logger->debug("searching cache for $key : $start..$end\n");
1424
1425         return undef unless $cache;
1426         my $data = $cache->get_cache($key);
1427
1428         return undef unless $data;
1429
1430         my $count = $data->[0];
1431         $data = $data->[1];
1432
1433         return undef unless $offset < $count;
1434
1435         my @result;
1436         for( my $i = $offset; $i <= $end; $i++ ) {
1437                 last unless my $d = $$data[$i];
1438                 push( @result, $d );
1439         }
1440
1441         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1442
1443         return \@result;
1444 }
1445
1446
1447 sub put_cache {
1448         my( $key, $count, $data ) = @_;
1449         return undef unless $cache;
1450         $logger->debug("search_cache putting ".
1451                 scalar(@$data)." items at key $key with timeout $cache_timeout");
1452         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1453 }
1454
1455
1456 __PACKAGE__->register_method(
1457     method   => "biblio_mrid_to_modsbatch_batch",
1458     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1459 );
1460
1461 sub biblio_mrid_to_modsbatch_batch {
1462         my( $self, $client, $mrids) = @_;
1463         # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1464         my @mods;
1465         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1466         for my $id (@$mrids) {
1467                 next unless defined $id;
1468                 my ($m) = $method->run($id);
1469                 push @mods, $m;
1470         }
1471         return \@mods;
1472 }
1473
1474
1475 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1476              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1477     {
1478     __PACKAGE__->register_method(
1479         method    => "biblio_mrid_to_modsbatch",
1480         api_name  => $_,
1481         signature => {
1482             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1483                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1484             params => [
1485                 { desc => 'Metarecord ID', type => 'number' },
1486                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1487             ],
1488             return => {
1489                 desc => 'MVR Object, event on error',
1490             }
1491         }
1492     );
1493 }
1494
1495 sub biblio_mrid_to_modsbatch {
1496         my( $self, $client, $mrid, $args) = @_;
1497
1498         # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1499
1500         my ($mr, $evt) = _grab_metarecord($mrid);
1501         return $evt unless $mr;
1502
1503         my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1504               biblio_mrid_make_modsbatch($self, $client, $mr);
1505
1506         return $mvr unless ref($args);  
1507
1508         # Here we find the lead record appropriate for the given filters 
1509         # and use that for the title and author of the metarecord
1510     my $format = $$args{format};
1511     my $org    = $$args{org};
1512     my $depth  = $$args{depth};
1513
1514         return $mvr unless $format or $org or $depth;
1515
1516         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1517         $method = "$method.staff" if $self->api_name =~ /staff/o; 
1518
1519         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1520
1521         if( my $mods = $U->record_to_mvr($rec) ) {
1522
1523         $mvr->title( $mods->title );
1524         $mvr->author($mods->author);
1525                 $logger->debug("mods_slim updating title and ".
1526                         "author in mvr with ".$mods->title." : ".$mods->author);
1527         }
1528
1529         return $mvr;
1530 }
1531
1532 # converts a metarecord to an mvr
1533 sub _mr_to_mvr {
1534         my $mr = shift;
1535         my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1536         return Fieldmapper::metabib::virtual_record->new($perl);
1537 }
1538
1539 # checks to see if a metarecord has mods, if so returns true;
1540
1541 __PACKAGE__->register_method(
1542     method   => "biblio_mrid_check_mvr",
1543     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1544     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1545               . "if the metarecord already has an mvr associated with it."
1546 );
1547
1548 sub biblio_mrid_check_mvr {
1549         my( $self, $client, $mrid ) = @_;
1550         my $mr; 
1551
1552         my $evt;
1553         if(ref($mrid)) { $mr = $mrid; } 
1554         else { ($mr, $evt) = _grab_metarecord($mrid); }
1555         return $evt if $evt;
1556
1557         # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1558
1559         return _mr_to_mvr($mr) if $mr->mods();
1560         return undef;
1561 }
1562
1563 sub _grab_metarecord {
1564         my $mrid = shift;
1565         #my $e = OpenILS::Utils::Editor->new;
1566         my $e = new_editor();
1567         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1568         return ($mr);
1569 }
1570
1571
1572 __PACKAGE__->register_method(
1573     method   => "biblio_mrid_make_modsbatch",
1574     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1575     notes    => "Takes either a metarecord ID or a metarecord object. "
1576               . "Forces the creations of an mvr for the given metarecord. "
1577               . "The created mvr is returned."
1578 );
1579
1580 sub biblio_mrid_make_modsbatch {
1581         my( $self, $client, $mrid ) = @_;
1582
1583         #my $e = OpenILS::Utils::Editor->new;
1584         my $e = new_editor();
1585
1586         my $mr;
1587         if( ref($mrid) ) {
1588                 $mr = $mrid;
1589                 $mrid = $mr->id;
1590         } else {
1591                 $mr = $e->retrieve_metabib_metarecord($mrid) 
1592                         or return $e->event;
1593         }
1594
1595         my $masterid = $mr->master_record;
1596         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1597
1598         my $ids = $U->storagereq(
1599                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1600         return undef unless @$ids;
1601
1602         my $master = $e->retrieve_biblio_record_entry($masterid)
1603                 or return $e->event;
1604
1605         # start the mods batch
1606         my $u = OpenILS::Utils::ModsParser->new();
1607         $u->start_mods_batch( $master->marc );
1608
1609         # grab all of the sub-records and shove them into the batch
1610         my @ids = grep { $_ ne $masterid } @$ids;
1611         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1612
1613         my $subrecs = [];
1614         if(@$ids) {
1615                 for my $i (@$ids) {
1616                         my $r = $e->retrieve_biblio_record_entry($i);
1617                         push( @$subrecs, $r ) if $r;
1618                 }
1619         }
1620
1621         for(@$subrecs) {
1622                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1623                 $u->push_mods_batch( $_->marc ) if $_->marc;
1624         }
1625
1626
1627         # finish up and send to the client
1628         my $mods = $u->finish_mods_batch();
1629         $mods->doc_id($mrid);
1630         $client->respond_complete($mods);
1631
1632
1633         # now update the mods string in the db
1634         my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1635         $mr->mods($string);
1636
1637         #$e = OpenILS::Utils::Editor->new(xact => 1);
1638         $e = new_editor(xact => 1);
1639         $e->update_metabib_metarecord($mr) 
1640                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1641         $e->finish;
1642
1643         return undef;
1644 }
1645
1646
1647 # converts a mr id into a list of record ids
1648
1649 foreach (qw/open-ils.search.biblio.metarecord_to_records
1650             open-ils.search.biblio.metarecord_to_records.staff/)
1651 {
1652     __PACKAGE__->register_method(
1653         method    => "biblio_mrid_to_record_ids",
1654         api_name  => $_,
1655         signature => {
1656             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1657                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1658             params => [
1659                 { desc => 'Metarecord ID', type => 'number' },
1660                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1661             ],
1662             return => {
1663                 desc => 'Results object like {count => $i, ids =>[...]}',
1664                 type => 'object'
1665             }
1666             
1667         }
1668     );
1669 }
1670
1671 sub biblio_mrid_to_record_ids {
1672         my( $self, $client, $mrid, $args ) = @_;
1673
1674     my $format = $$args{format};
1675     my $org    = $$args{org};
1676     my $depth  = $$args{depth};
1677
1678         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1679         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1680         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1681
1682         return { count => scalar(@$recs), ids => $recs };
1683 }
1684
1685
1686 __PACKAGE__->register_method(
1687     method   => "biblio_record_to_marc_html",
1688     api_name => "open-ils.search.biblio.record.html"
1689 );
1690
1691 __PACKAGE__->register_method(
1692     method   => "biblio_record_to_marc_html",
1693     api_name => "open-ils.search.authority.to_html"
1694 );
1695
1696 # Persistent parsers and setting objects
1697 my $parser = XML::LibXML->new();
1698 my $xslt   = XML::LibXSLT->new();
1699 my $marc_sheet;
1700 my $slim_marc_sheet;
1701 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1702
1703 sub biblio_record_to_marc_html {
1704         my($self, $client, $recordid, $slim, $marcxml) = @_;
1705
1706     my $sheet;
1707         my $dir = $settings_client->config_value("dirs", "xsl");
1708
1709     if($slim) {
1710         unless($slim_marc_sheet) {
1711                     my $xsl = $settings_client->config_value(
1712                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1713             if($xsl) {
1714                         $xsl = $parser->parse_file("$dir/$xsl");
1715                         $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1716             }
1717         }
1718         $sheet = $slim_marc_sheet;
1719     }
1720
1721     unless($sheet) {
1722         unless($marc_sheet) {
1723             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1724                     my $xsl = $settings_client->config_value(
1725                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1726                     $xsl = $parser->parse_file("$dir/$xsl");
1727                     $marc_sheet = $xslt->parse_stylesheet($xsl);
1728         }
1729         $sheet = $marc_sheet;
1730     }
1731
1732     my $record;
1733     unless($marcxml) {
1734         my $e = new_editor();
1735         if($self->api_name =~ /authority/) {
1736             $record = $e->retrieve_authority_record_entry($recordid)
1737                 or return $e->event;
1738         } else {
1739             $record = $e->retrieve_biblio_record_entry($recordid)
1740                 or return $e->event;
1741         }
1742         $marcxml = $record->marc;
1743     }
1744
1745         my $xmldoc = $parser->parse_string($marcxml);
1746         my $html = $sheet->transform($xmldoc);
1747         return $html->documentElement->toString();
1748 }
1749
1750 __PACKAGE__->register_method(
1751     method    => "format_biblio_record_entry",
1752     api_name  => "open-ils.search.biblio.record.print",
1753     signature => {
1754         desc   => 'Returns a printable version of the specified bib record',
1755         params => [
1756             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1757         ],
1758         return => {
1759             desc => q/An action_trigger.event object or error event./,
1760             type => 'object',
1761         }
1762     }
1763 );
1764 __PACKAGE__->register_method(
1765     method    => "format_biblio_record_entry",
1766     api_name  => "open-ils.search.biblio.record.email",
1767     signature => {
1768         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1769         params => [
1770             { desc => 'Authentication token',  type => 'string'},
1771             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1772         ],
1773         return => {
1774             desc => q/Undefined on success, otherwise an error event./,
1775             type => 'object',
1776         }
1777     }
1778 );
1779
1780 sub format_biblio_record_entry {
1781     my($self, $conn, $arg1, $arg2) = @_;
1782
1783     my $for_print = ($self->api_name =~ /print/);
1784     my $for_email = ($self->api_name =~ /email/);
1785
1786     my $e; my $auth; my $bib_id; my $context_org;
1787
1788     if ($for_print) {
1789         $bib_id = $arg1;
1790         $context_org = $arg2 || $U->fetch_org_tree->id;
1791         $e = new_editor(xact => 1);
1792     } elsif ($for_email) {
1793         $auth = $arg1;
1794         $bib_id = $arg2;
1795         $e = new_editor(authtoken => $auth, xact => 1);
1796         return $e->die_event unless $e->checkauth;
1797         $context_org = $e->requestor->home_ou;
1798     }
1799
1800     my $bib_ids;
1801     if (ref $bib_id ne 'ARRAY') {
1802         $bib_ids = [ $bib_id ];
1803     } else {
1804         $bib_ids = $bib_id;
1805     }
1806
1807     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1808     $bucket->btype('temp');
1809     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1810     if ($for_email) {
1811         $bucket->owner($e->requestor) 
1812     } else {
1813         $bucket->owner(1);
1814     }
1815     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1816
1817     for my $id (@$bib_ids) {
1818
1819         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1820
1821         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1822         $bucket_entry->target_biblio_record_entry($bib);
1823         $bucket_entry->bucket($bucket_obj->id);
1824         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1825     }
1826
1827     $e->commit;
1828
1829     if ($for_print) {
1830
1831         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1832
1833     } elsif ($for_email) {
1834
1835         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1836     }
1837
1838     return undef;
1839 }
1840
1841
1842 __PACKAGE__->register_method(
1843     method   => "retrieve_all_copy_statuses",
1844     api_name => "open-ils.search.config.copy_status.retrieve.all"
1845 );
1846
1847 sub retrieve_all_copy_statuses {
1848         my( $self, $client ) = @_;
1849         return new_editor()->retrieve_all_config_copy_status();
1850 }
1851
1852
1853 __PACKAGE__->register_method(
1854     method   => "copy_counts_per_org",
1855     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1856 );
1857
1858 __PACKAGE__->register_method(
1859     method   => "copy_counts_per_org",
1860     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1861 );
1862
1863 sub copy_counts_per_org {
1864         my( $self, $client, $record_id ) = @_;
1865
1866         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1867
1868         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1869         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1870
1871         my $counts = $apputils->simple_scalar_request(
1872                 "open-ils.storage", $method, $record_id );
1873
1874         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1875         return $counts;
1876 }
1877
1878
1879 __PACKAGE__->register_method(
1880     method   => "copy_count_summary",
1881     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1882     notes    => "returns an array of these: "
1883               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1884               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1885 );
1886                 
1887
1888 sub copy_count_summary {
1889         my( $self, $client, $rid, $org, $depth ) = @_;
1890     $org   ||= 1;
1891     $depth ||= 0;
1892     my $data = $U->storagereq(
1893                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1894
1895     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1896 }
1897
1898 __PACKAGE__->register_method(
1899     method   => "copy_location_count_summary",
1900     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1901     notes    => "returns an array of these: "
1902               . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1903               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1904 );
1905
1906 sub copy_location_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_location_count.atomic', $rid, $org, $depth );
1912
1913     return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1914 }
1915
1916 __PACKAGE__->register_method(
1917     method   => "copy_count_location_summary",
1918     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1919     notes    => "returns an array of these: "
1920               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1921               . "where statusx is a copy status name.  The statuses are sorted by ID."
1922 );
1923
1924 sub copy_count_location_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     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1931 }
1932
1933
1934 foreach (qw/open-ils.search.biblio.marc
1935             open-ils.search.biblio.marc.staff/)
1936 {
1937 __PACKAGE__->register_method(
1938     method    => "marc_search",
1939     api_name  => $_,
1940     signature => {
1941         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
1942                 . 'As usual, the .staff version of the search includes otherwise hidden records',
1943         params => [
1944             {
1945                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1946                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
1947                 type => 'object'
1948             },
1949             {desc => 'limit (optional)',  type => 'number'},
1950             {desc => 'offset (optional)', type => 'number'}
1951         ],
1952         return => {
1953             desc => 'Results object like: { "count": $i, "ids": [...] }',
1954             type => 'object'
1955         }
1956     }
1957 );
1958 }
1959
1960 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1961
1962 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
1963
1964     searches: complex query object  (required)
1965     org_unit: The org ID to focus the search at
1966     depth   : The org depth     
1967     limit   : integer search limit      default: 10
1968     offset  : integer search offset     default:  0
1969     sort    : What field to sort the results on? [ author | title | pubdate ]
1970     sort_dir: In what direction do we sort? [ asc | desc ]
1971
1972 Additional keys to refine search criteria:
1973
1974     audience : Audience
1975     language : Language (code)
1976     lit_form : Literary form
1977     item_form: Item form
1978     item_type: Item type
1979     format   : The MARC format
1980
1981 Please note that the specific strings to be used in the "addtional keys" will be entirely
1982 dependent on your loaded data.  
1983
1984 All keys except "searches" are optional.
1985 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
1986
1987 For example, an arg hash might look like:
1988
1989     $arghash = {
1990         searches => [
1991             {
1992                 term     => "harry",
1993                 restrict => [
1994                     {
1995                         tag => 245,
1996                         subfield => "a"
1997                     }
1998                     # ...
1999                 ]
2000             }
2001             # ...
2002         ],
2003         org_unit  => 1,
2004         limit     => 5,
2005         sort      => "author",
2006         item_type => "g"
2007     }
2008
2009 The arghash is eventually passed to the SRF call:
2010 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
2011
2012 Presently, search uses the cache unconditionally.
2013
2014 =cut
2015
2016 # FIXME: that example above isn't actually tested.
2017 # TODO: docache option?
2018 sub marc_search {
2019         my( $self, $conn, $args, $limit, $offset ) = @_;
2020
2021         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2022         $method .= ".staff" if $self->api_name =~ /staff/;
2023         $method .= ".atomic";
2024
2025     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
2026     $offset ||=  0;     # FIXME: what about $args->{offset} ?
2027
2028         my @search;
2029         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2030         my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2031
2032         my $recs = search_cache($ckey, $offset, $limit);
2033
2034         if(!$recs) {
2035                 $recs = $U->storagereq($method, %$args) || [];
2036                 if( $recs ) {
2037                         put_cache($ckey, scalar(@$recs), $recs);
2038                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2039                 } else {
2040                         $recs = [];
2041                 }
2042         }
2043
2044         my $count = 0;
2045         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2046         my @recs = map { $_->[0] } @$recs;
2047
2048         return { ids => \@recs, count => $count };
2049 }
2050
2051
2052 __PACKAGE__->register_method(
2053     method    => "biblio_search_isbn",
2054     api_name  => "open-ils.search.biblio.isbn",
2055     signature => {
2056         desc   => 'Retrieve biblio IDs for a given ISBN',
2057         params => [
2058             {desc => 'ISBN', type => 'string'}  # or number maybe?  How normalized is our storage data?
2059         ],
2060         return => {
2061             desc => 'Results object like: { "count": $i, "ids": [...] }',
2062             type => 'object'
2063         }
2064     }
2065 );
2066
2067 sub biblio_search_isbn { 
2068         my( $self, $client, $isbn ) = @_;
2069         $logger->debug("Searching ISBN $isbn");
2070         my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2071         return { ids => $recs, count => scalar(@$recs) };
2072 }
2073
2074 __PACKAGE__->register_method(
2075     method   => "biblio_search_isbn_batch",
2076     api_name => "open-ils.search.biblio.isbn_list",
2077 );
2078
2079 sub biblio_search_isbn_batch { 
2080         my( $self, $client, $isbn_list ) = @_;
2081         $logger->debug("Searching ISBNs @$isbn_list");
2082         my @recs = (); my %rec_set = ();
2083         foreach my $isbn ( @$isbn_list ) {
2084                 foreach my $rec ( @{ $U->storagereq(
2085                         'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2086                 } ) {
2087                         if (! $rec_set{ $rec }) {
2088                                 $rec_set{ $rec } = 1;
2089                                 push @recs, $rec;
2090                         }
2091                 }
2092         }
2093         return { ids => \@recs, count => scalar(@recs) };
2094 }
2095
2096 __PACKAGE__->register_method(
2097     method   => "biblio_search_issn",
2098     api_name => "open-ils.search.biblio.issn",
2099     signature => {
2100         desc   => 'Retrieve biblio IDs for a given ISSN',
2101         params => [
2102             {desc => 'ISBN', type => 'string'}
2103         ],
2104         return => {
2105             desc => 'Results object like: { "count": $i, "ids": [...] }',
2106             type => 'object'
2107         }
2108     }
2109 );
2110
2111 sub biblio_search_issn { 
2112         my( $self, $client, $issn ) = @_;
2113         $logger->debug("Searching ISSN $issn");
2114         my $e = new_editor();
2115         $issn =~ s/-/ /g;
2116         my $recs = $U->storagereq(
2117                 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2118         return { ids => $recs, count => scalar(@$recs) };
2119 }
2120
2121
2122 __PACKAGE__->register_method(
2123     method    => "fetch_mods_by_copy",
2124     api_name  => "open-ils.search.biblio.mods_from_copy",
2125     argc      => 1,
2126     signature => {
2127         desc    => 'Retrieve MODS record given an attached copy ID',
2128         params  => [
2129             { desc => 'Copy ID', type => 'number' }
2130         ],
2131         returns => {
2132             desc => 'MODS record, event on error or uncataloged item'
2133         }
2134     }
2135 );
2136
2137 sub fetch_mods_by_copy {
2138         my( $self, $client, $copyid ) = @_;
2139         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2140         return $evt if $evt;
2141         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2142         return $apputils->record_to_mvr($record);
2143 }
2144
2145
2146 # -------------------------------------------------------------------------------------
2147
2148 __PACKAGE__->register_method(
2149     method   => "cn_browse",
2150     api_name => "open-ils.search.callnumber.browse.target",
2151     notes    => "Starts a callnumber browse"
2152 );
2153
2154 __PACKAGE__->register_method(
2155     method   => "cn_browse",
2156     api_name => "open-ils.search.callnumber.browse.page_up",
2157     notes    => "Returns the previous page of callnumbers",
2158 );
2159
2160 __PACKAGE__->register_method(
2161     method   => "cn_browse",
2162     api_name => "open-ils.search.callnumber.browse.page_down",
2163     notes    => "Returns the next page of callnumbers",
2164 );
2165
2166
2167 # RETURNS array of arrays like so: label, owning_lib, record, id
2168 sub cn_browse {
2169         my( $self, $client, @params ) = @_;
2170         my $method;
2171
2172         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2173                 if( $self->api_name =~ /target/ );
2174         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2175                 if( $self->api_name =~ /page_up/ );
2176         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2177                 if( $self->api_name =~ /page_down/ );
2178
2179         return $apputils->simplereq( 'open-ils.storage', $method, @params );
2180 }
2181 # -------------------------------------------------------------------------------------
2182
2183 __PACKAGE__->register_method(
2184     method        => "fetch_cn",
2185     api_name      => "open-ils.search.callnumber.retrieve",
2186     authoritative => 1,
2187     notes         => "retrieves a callnumber based on ID",
2188 );
2189
2190 sub fetch_cn {
2191         my( $self, $client, $id ) = @_;
2192         my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2193         return $evt if $evt;
2194         return $cn;
2195 }
2196
2197 __PACKAGE__->register_method(
2198     method    => "fetch_copy_by_cn",
2199     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2200     signature => q/
2201                 Returns an array of copy ID's by callnumber ID
2202                 @param cnid The callnumber ID
2203                 @return An array of copy IDs
2204         /
2205 );
2206
2207 sub fetch_copy_by_cn {
2208         my( $self, $conn, $cnid ) = @_;
2209         return $U->cstorereq(
2210                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2211                 { call_number => $cnid, deleted => 'f' } );
2212 }
2213
2214 __PACKAGE__->register_method(
2215     method    => 'fetch_cn_by_info',
2216     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2217     signature => q/
2218                 @param label The callnumber label
2219                 @param record The record the cn is attached to
2220                 @param org The owning library of the cn
2221                 @return The callnumber object
2222         /
2223 );
2224
2225
2226 sub fetch_cn_by_info {
2227         my( $self, $conn, $label, $record, $org ) = @_;
2228         return $U->cstorereq(
2229                 'open-ils.cstore.direct.asset.call_number.search',
2230                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2231 }
2232
2233
2234
2235 __PACKAGE__->register_method(
2236     method   => 'bib_extras',
2237     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2238 );
2239 __PACKAGE__->register_method(
2240     method   => 'bib_extras',
2241     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2242 );
2243 __PACKAGE__->register_method(
2244     method   => 'bib_extras',
2245     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2246 );
2247 __PACKAGE__->register_method(
2248     method   => 'bib_extras',
2249     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2250 );
2251 __PACKAGE__->register_method(
2252     method   => 'bib_extras',
2253     api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2254 );
2255
2256 sub bib_extras {
2257         my $self = shift;
2258
2259         my $e = new_editor();
2260
2261         return $e->retrieve_all_config_lit_form_map()
2262                 if( $self->api_name =~ /lit_form/ );
2263
2264         return $e->retrieve_all_config_item_form_map()
2265                 if( $self->api_name =~ /item_form_map/ );
2266
2267         return $e->retrieve_all_config_item_type_map()
2268                 if( $self->api_name =~ /item_type_map/ );
2269
2270         return $e->retrieve_all_config_bib_level_map()
2271                 if( $self->api_name =~ /bib_level_map/ );
2272
2273         return $e->retrieve_all_config_audience_map()
2274                 if( $self->api_name =~ /audience_map/ );
2275
2276         return [];
2277 }
2278
2279
2280
2281 __PACKAGE__->register_method(
2282     method    => 'fetch_slim_record',
2283     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2284     signature => {
2285         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2286         params => [
2287             { desc => 'Array of Record IDs', type => 'array' }
2288         ],
2289         return => { 
2290             desc => 'Array of biblio records, event on error'
2291         }
2292     }
2293 );
2294
2295 sub fetch_slim_record {
2296     my( $self, $conn, $ids ) = @_;
2297
2298 #my $editor = OpenILS::Utils::Editor->new;
2299     my $editor = new_editor();
2300         my @res;
2301     for( @$ids ) {
2302         return $editor->event unless
2303             my $r = $editor->retrieve_biblio_record_entry($_);
2304         $r->clear_marc;
2305         push(@res, $r);
2306     }
2307     return \@res;
2308 }
2309
2310
2311
2312 __PACKAGE__->register_method(
2313     method    => 'rec_to_mr_rec_descriptors',
2314     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2315     signature => q/
2316                 specialized method...
2317                 Given a biblio record id or a metarecord id, 
2318                 this returns a list of metabib.record_descriptor
2319                 objects that live within the same metarecord
2320                 @param args Object of args including:
2321         /
2322 );
2323
2324 sub rec_to_mr_rec_descriptors {
2325         my( $self, $conn, $args ) = @_;
2326
2327     my $rec        = $$args{record};
2328     my $mrec       = $$args{metarecord};
2329     my $item_forms = $$args{item_forms};
2330     my $item_types = $$args{item_types};
2331     my $item_lang  = $$args{item_lang};
2332
2333         my $e = new_editor();
2334         my $recs;
2335
2336         if( !$mrec ) {
2337                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2338                 return $e->event unless @$map;
2339                 $mrec = $$map[0]->metarecord;
2340         }
2341
2342         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2343         return $e->event unless @$recs;
2344
2345         my @recs = map { $_->source } @$recs;
2346         my $search = { record => \@recs };
2347         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2348         $search->{item_type} = $item_types if $item_types and @$item_types;
2349         $search->{item_lang} = $item_lang  if $item_lang;
2350
2351         my $desc = $e->search_metabib_record_descriptor($search);
2352
2353         return { metarecord => $mrec, descriptors => $desc };
2354 }
2355
2356
2357 __PACKAGE__->register_method(
2358     method   => 'fetch_age_protect',
2359     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2360 );
2361
2362 sub fetch_age_protect {
2363         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2364 }
2365
2366
2367 __PACKAGE__->register_method(
2368     method   => 'copies_by_cn_label',
2369     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2370 );
2371
2372 __PACKAGE__->register_method(
2373     method   => 'copies_by_cn_label',
2374     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2375 );
2376
2377 sub copies_by_cn_label {
2378         my( $self, $conn, $record, $label, $circ_lib ) = @_;
2379         my $e = new_editor();
2380         my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2381         return [] unless @$cns;
2382
2383         # show all non-deleted copies in the staff client ...
2384         if ($self->api_name =~ /staff$/o) {
2385                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2386         }
2387
2388         # ... otherwise, grab the copies ...
2389         my $copies = $e->search_asset_copy(
2390                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2391                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2392                 ]
2393         );
2394
2395         # ... and test for location and status visibility
2396         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2397 }
2398
2399
2400 1;
2401