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