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