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