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