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