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