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