]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Search/Biblio.pm
Patch from Joe Atzberger: More method reg docs in Biblio.pm and Actor
[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) if $docache;
1182     return undef;
1183 }
1184
1185 # creates a unique token to represent the query in the cache
1186 sub search_cache_key {
1187     my $method = shift;
1188     my $search_hash = shift;
1189         my @sorted;
1190     for my $key (sort keys %$search_hash) {
1191             push(@sorted, ($key => $$search_hash{$key})) 
1192             unless $key eq 'limit'  or 
1193                    $key eq 'offset' or 
1194                    $key eq 'skip_check';
1195     }
1196         my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1197         return $pfx . md5_hex($method . $s);
1198 }
1199
1200 sub retrieve_cached_facets {
1201     my $self   = shift;
1202     my $client = shift;
1203     my $key    = shift;
1204
1205     return undef unless ($key and $key =~ /_facets$/);
1206
1207     return $cache->get_cache($key) || {};
1208 }
1209
1210 __PACKAGE__->register_method(
1211     method   => "retrieve_cached_facets",
1212     api_name => "open-ils.search.facet_cache.retrieve"
1213 );
1214
1215
1216 sub cache_facets {
1217     # add facets for this search to the facet cache
1218     my($key, $results) = @_;
1219     my $data = $cache->get_cache($key);
1220     $data ||= {};
1221
1222     return undef unless (@$results);
1223
1224     # The query we're constructing
1225     #
1226     # select  cmf.id,
1227     #         mfae.value,
1228     #         count(distinct mfae.source)
1229     #   from  metabib.facet_entry mfae
1230     #         join config.metabib_field cmf on (mfae.field = cmf.id)
1231     #   where cmf.facet_field
1232     #         and mfae.source in IDLIST
1233     #   group by 1,2;
1234
1235     my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1236         {   select  => {
1237                 cmf  => [ 'id' ],
1238                 mfae => [ 
1239                     'value',
1240                     {
1241                         transform => 'count',
1242                         distinct => 1,
1243                         column => 'source',
1244                         alias => 'count',
1245                         aggregate => 1
1246                     }
1247                 ]
1248             },
1249             from    => { mfae => 'cmf' },
1250             where   => { '+cmf'  => 'facet_field', '+mfae' => { source => $results } }
1251         }
1252     );
1253
1254     for my $facet (@$facets) {
1255         next unless ($facet->{value});
1256         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1257     }
1258
1259     $logger->info("facet compilation: cached with key=$key");
1260
1261     $cache->put_cache($key, $data, $cache_timeout);
1262 }
1263
1264 sub cache_staged_search_page {
1265     # puts this set of results into the cache
1266     my($key, $page, $summary, $results) = @_;
1267     my $data = $cache->get_cache($key);
1268     $data ||= {};
1269     $data->{$page} = {
1270         summary => $summary,
1271         results => $results
1272     };
1273
1274     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1275         $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1276
1277     $cache->put_cache($key, $data, $cache_timeout);
1278 }
1279
1280 sub search_cache {
1281
1282         my $key         = shift;
1283         my $offset      = shift;
1284         my $limit       = shift;
1285         my $start       = $offset;
1286         my $end         = $offset + $limit - 1;
1287
1288         $logger->debug("searching cache for $key : $start..$end\n");
1289
1290         return undef unless $cache;
1291         my $data = $cache->get_cache($key);
1292
1293         return undef unless $data;
1294
1295         my $count = $data->[0];
1296         $data = $data->[1];
1297
1298         return undef unless $offset < $count;
1299
1300         my @result;
1301         for( my $i = $offset; $i <= $end; $i++ ) {
1302                 last unless my $d = $$data[$i];
1303                 push( @result, $d );
1304         }
1305
1306         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1307
1308         return \@result;
1309 }
1310
1311
1312 sub put_cache {
1313         my( $key, $count, $data ) = @_;
1314         return undef unless $cache;
1315         $logger->debug("search_cache putting ".
1316                 scalar(@$data)." items at key $key with timeout $cache_timeout");
1317         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1318 }
1319
1320
1321 __PACKAGE__->register_method(
1322     method   => "biblio_mrid_to_modsbatch_batch",
1323     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1324 );
1325
1326 sub biblio_mrid_to_modsbatch_batch {
1327         my( $self, $client, $mrids) = @_;
1328         # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1329         my @mods;
1330         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1331         for my $id (@$mrids) {
1332                 next unless defined $id;
1333                 my ($m) = $method->run($id);
1334                 push @mods, $m;
1335         }
1336         return \@mods;
1337 }
1338
1339
1340 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1341              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1342     {
1343     __PACKAGE__->register_method(
1344         method    => "biblio_mrid_to_modsbatch",
1345         api_name  => $_,
1346         signature => {
1347             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1348                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1349             params => [
1350                 { desc => 'Metarecord ID', type => 'number' },
1351                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1352             ],
1353             return => {
1354                 desc => 'MVR Object, event on error',
1355             }
1356         }
1357     );
1358 }
1359
1360 sub biblio_mrid_to_modsbatch {
1361         my( $self, $client, $mrid, $args) = @_;
1362
1363         # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1364
1365         my ($mr, $evt) = _grab_metarecord($mrid);
1366         return $evt unless $mr;
1367
1368         my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1369               biblio_mrid_make_modsbatch($self, $client, $mr);
1370
1371         return $mvr unless ref($args);  
1372
1373         # Here we find the lead record appropriate for the given filters 
1374         # and use that for the title and author of the metarecord
1375     my $format = $$args{format};
1376     my $org    = $$args{org};
1377     my $depth  = $$args{depth};
1378
1379         return $mvr unless $format or $org or $depth;
1380
1381         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1382         $method = "$method.staff" if $self->api_name =~ /staff/o; 
1383
1384         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1385
1386         if( my $mods = $U->record_to_mvr($rec) ) {
1387
1388         $mvr->title( $mods->title );
1389         $mvr->author($mods->author);
1390                 $logger->debug("mods_slim updating title and ".
1391                         "author in mvr with ".$mods->title." : ".$mods->author);
1392         }
1393
1394         return $mvr;
1395 }
1396
1397 # converts a metarecord to an mvr
1398 sub _mr_to_mvr {
1399         my $mr = shift;
1400         my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1401         return Fieldmapper::metabib::virtual_record->new($perl);
1402 }
1403
1404 # checks to see if a metarecord has mods, if so returns true;
1405
1406 __PACKAGE__->register_method(
1407     method   => "biblio_mrid_check_mvr",
1408     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1409     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1410               . "if the metarecord already has an mvr associated with it."
1411 );
1412
1413 sub biblio_mrid_check_mvr {
1414         my( $self, $client, $mrid ) = @_;
1415         my $mr; 
1416
1417         my $evt;
1418         if(ref($mrid)) { $mr = $mrid; } 
1419         else { ($mr, $evt) = _grab_metarecord($mrid); }
1420         return $evt if $evt;
1421
1422         # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1423
1424         return _mr_to_mvr($mr) if $mr->mods();
1425         return undef;
1426 }
1427
1428 sub _grab_metarecord {
1429         my $mrid = shift;
1430         #my $e = OpenILS::Utils::Editor->new;
1431         my $e = new_editor();
1432         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1433         return ($mr);
1434 }
1435
1436
1437 __PACKAGE__->register_method(
1438     method   => "biblio_mrid_make_modsbatch",
1439     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1440     notes    => "Takes either a metarecord ID or a metarecord object. "
1441               . "Forces the creations of an mvr for the given metarecord. "
1442               . "The created mvr is returned."
1443 );
1444
1445 sub biblio_mrid_make_modsbatch {
1446         my( $self, $client, $mrid ) = @_;
1447
1448         #my $e = OpenILS::Utils::Editor->new;
1449         my $e = new_editor();
1450
1451         my $mr;
1452         if( ref($mrid) ) {
1453                 $mr = $mrid;
1454                 $mrid = $mr->id;
1455         } else {
1456                 $mr = $e->retrieve_metabib_metarecord($mrid) 
1457                         or return $e->event;
1458         }
1459
1460         my $masterid = $mr->master_record;
1461         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1462
1463         my $ids = $U->storagereq(
1464                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1465         return undef unless @$ids;
1466
1467         my $master = $e->retrieve_biblio_record_entry($masterid)
1468                 or return $e->event;
1469
1470         # start the mods batch
1471         my $u = OpenILS::Utils::ModsParser->new();
1472         $u->start_mods_batch( $master->marc );
1473
1474         # grab all of the sub-records and shove them into the batch
1475         my @ids = grep { $_ ne $masterid } @$ids;
1476         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1477
1478         my $subrecs = [];
1479         if(@$ids) {
1480                 for my $i (@$ids) {
1481                         my $r = $e->retrieve_biblio_record_entry($i);
1482                         push( @$subrecs, $r ) if $r;
1483                 }
1484         }
1485
1486         for(@$subrecs) {
1487                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1488                 $u->push_mods_batch( $_->marc ) if $_->marc;
1489         }
1490
1491
1492         # finish up and send to the client
1493         my $mods = $u->finish_mods_batch();
1494         $mods->doc_id($mrid);
1495         $client->respond_complete($mods);
1496
1497
1498         # now update the mods string in the db
1499         my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1500         $mr->mods($string);
1501
1502         #$e = OpenILS::Utils::Editor->new(xact => 1);
1503         $e = new_editor(xact => 1);
1504         $e->update_metabib_metarecord($mr) 
1505                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1506         $e->finish;
1507
1508         return undef;
1509 }
1510
1511
1512 # converts a mr id into a list of record ids
1513
1514 foreach (qw/open-ils.search.biblio.metarecord_to_records
1515             open-ils.search.biblio.metarecord_to_records.staff/)
1516 {
1517     __PACKAGE__->register_method(
1518         method    => "biblio_mrid_to_record_ids",
1519         api_name  => $_,
1520         signature => {
1521             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1522                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1523             params => [
1524                 { desc => 'Metarecord ID', type => 'number' },
1525                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1526             ],
1527             return => {
1528                 desc => 'Results object like {count => $i, ids =>[...]}',
1529                 type => 'object'
1530             }
1531             
1532         }
1533     );
1534 }
1535
1536 sub biblio_mrid_to_record_ids {
1537         my( $self, $client, $mrid, $args ) = @_;
1538
1539     my $format = $$args{format};
1540     my $org    = $$args{org};
1541     my $depth  = $$args{depth};
1542
1543         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1544         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1545         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1546
1547         return { count => scalar(@$recs), ids => $recs };
1548 }
1549
1550
1551 __PACKAGE__->register_method(
1552     method   => "biblio_record_to_marc_html",
1553     api_name => "open-ils.search.biblio.record.html"
1554 );
1555
1556 __PACKAGE__->register_method(
1557     method   => "biblio_record_to_marc_html",
1558     api_name => "open-ils.search.authority.to_html"
1559 );
1560
1561 # Persistent parsers and setting objects
1562 my $parser = XML::LibXML->new();
1563 my $xslt   = XML::LibXSLT->new();
1564 my $marc_sheet;
1565 my $slim_marc_sheet;
1566 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1567
1568 sub biblio_record_to_marc_html {
1569         my($self, $client, $recordid, $slim, $marcxml) = @_;
1570
1571     my $sheet;
1572         my $dir = $settings_client->config_value("dirs", "xsl");
1573
1574     if($slim) {
1575         unless($slim_marc_sheet) {
1576                     my $xsl = $settings_client->config_value(
1577                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1578             if($xsl) {
1579                         $xsl = $parser->parse_file("$dir/$xsl");
1580                         $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1581             }
1582         }
1583         $sheet = $slim_marc_sheet;
1584     }
1585
1586     unless($sheet) {
1587         unless($marc_sheet) {
1588             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1589                     my $xsl = $settings_client->config_value(
1590                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1591                     $xsl = $parser->parse_file("$dir/$xsl");
1592                     $marc_sheet = $xslt->parse_stylesheet($xsl);
1593         }
1594         $sheet = $marc_sheet;
1595     }
1596
1597     my $record;
1598     unless($marcxml) {
1599         my $e = new_editor();
1600         if($self->api_name =~ /authority/) {
1601             $record = $e->retrieve_authority_record_entry($recordid)
1602                 or return $e->event;
1603         } else {
1604             $record = $e->retrieve_biblio_record_entry($recordid)
1605                 or return $e->event;
1606         }
1607         $marcxml = $record->marc;
1608     }
1609
1610         my $xmldoc = $parser->parse_string($marcxml);
1611         my $html = $sheet->transform($xmldoc);
1612         return $html->documentElement->toString();
1613 }
1614
1615
1616
1617 __PACKAGE__->register_method(
1618     method   => "retrieve_all_copy_statuses",
1619     api_name => "open-ils.search.config.copy_status.retrieve.all"
1620 );
1621
1622 sub retrieve_all_copy_statuses {
1623         my( $self, $client ) = @_;
1624         return new_editor()->retrieve_all_config_copy_status();
1625 }
1626
1627
1628 __PACKAGE__->register_method(
1629     method   => "copy_counts_per_org",
1630     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1631 );
1632
1633 __PACKAGE__->register_method(
1634     method   => "copy_counts_per_org",
1635     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1636 );
1637
1638 sub copy_counts_per_org {
1639         my( $self, $client, $record_id ) = @_;
1640
1641         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1642
1643         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1644         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1645
1646         my $counts = $apputils->simple_scalar_request(
1647                 "open-ils.storage", $method, $record_id );
1648
1649         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1650         return $counts;
1651 }
1652
1653
1654 __PACKAGE__->register_method(
1655     method   => "copy_count_summary",
1656     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1657     notes    => "returns an array of these: "
1658               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1659               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1660 );
1661                 
1662
1663 sub copy_count_summary {
1664         my( $self, $client, $rid, $org, $depth ) = @_;
1665     $org   ||= 1;
1666     $depth ||= 0;
1667     my $data = $U->storagereq(
1668                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1669
1670     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1671 }
1672
1673 __PACKAGE__->register_method(
1674     method   => "copy_location_count_summary",
1675     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1676     notes    => "returns an array of these: "
1677               . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1678               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1679 );
1680
1681 sub copy_location_count_summary {
1682     my( $self, $client, $rid, $org, $depth ) = @_;
1683     $org   ||= 1;
1684     $depth ||= 0;
1685     my $data = $U->storagereq(
1686                 'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1687
1688     return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1689 }
1690
1691 __PACKAGE__->register_method(
1692     method   => "copy_count_location_summary",
1693     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1694     notes    => "returns an array of these: "
1695               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1696               . "where statusx is a copy status name.  The statuses are sorted by ID."
1697 );
1698
1699 sub copy_count_location_summary {
1700     my( $self, $client, $rid, $org, $depth ) = @_;
1701     $org   ||= 1;
1702     $depth ||= 0;
1703     my $data = $U->storagereq(
1704         'open-ils.storage.biblio.record_entry.status_copy_location_count.atomic', $rid, $org, $depth );
1705     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1706 }
1707
1708
1709 foreach (qw/open-ils.search.biblio.marc
1710             open-ils.search.biblio.marc.staff/)
1711 {
1712 __PACKAGE__->register_method(
1713     method    => "marc_search",
1714     api_name  => $_,
1715     signature => {
1716         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
1717                 . 'As usual, the .staff version of the search includes otherwise hidden records',
1718         params => [
1719             {
1720                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1721                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
1722                 type => 'object'
1723             },
1724             {desc => 'limit (optional)',  type => 'number'},
1725             {desc => 'offset (optional)', type => 'number'}
1726         ],
1727         return => {
1728             desc => 'Results object like: { "count": $i, "ids": [...] }',
1729             type => 'object'
1730         }
1731     }
1732 );
1733 }
1734
1735 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1736
1737 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
1738
1739     searches: complex query object  (required)
1740     org_unit: The org ID to focus the search at
1741     depth   : The org depth     
1742     limit   : integer search limit      default: 10
1743     offset  : integer search offset     default:  0
1744     sort    : What field to sort the results on? [ author | title | pubdate ]
1745     sort_dir: In what direction do we sort? [ asc | desc ]
1746
1747 Additional keys to refine search criteria:
1748
1749     audience : Audience
1750     language : Language (code)
1751     lit_form : Literary form
1752     item_form: Item form
1753     item_type: Item type
1754     format   : The MARC format
1755
1756 Please note that the specific strings to be used in the "addtional keys" will be entirely
1757 dependent on your loaded data.  
1758
1759 All keys except "searches" are optional.
1760 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
1761
1762 For example, an arg hash might look like:
1763
1764     $arghash = {
1765         searches => [
1766             {
1767                 term     => "harry",
1768                 restrict => [
1769                     {
1770                         tag => 245,
1771                         subfield => "a"
1772                     }
1773                     # ...
1774                 ]
1775             }
1776             # ...
1777         ],
1778         org_unit  => 1,
1779         limit     => 5,
1780         sort      => "author",
1781         item_type => "g"
1782     }
1783
1784 The arghash is eventually passed to the SRF call:
1785 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1786
1787 Presently, search uses the cache unconditionally.
1788
1789 =cut
1790
1791 # FIXME: that example above isn't actually tested.
1792 # TODO: docache option?
1793 sub marc_search {
1794         my( $self, $conn, $args, $limit, $offset ) = @_;
1795
1796         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
1797         $method .= ".staff" if $self->api_name =~ /staff/;
1798         $method .= ".atomic";
1799
1800     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
1801     $offset ||=  0;     # FIXME: what about $args->{offset} ?
1802
1803         my @search;
1804         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
1805         my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
1806
1807         my $recs = search_cache($ckey, $offset, $limit);
1808
1809         if(!$recs) {
1810                 $recs = $U->storagereq($method, %$args) || [];
1811                 if( $recs ) {
1812                         put_cache($ckey, scalar(@$recs), $recs);
1813                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
1814                 } else {
1815                         $recs = [];
1816                 }
1817         }
1818
1819         my $count = 0;
1820         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
1821         my @recs = map { $_->[0] } @$recs;
1822
1823         return { ids => \@recs, count => $count };
1824 }
1825
1826
1827 __PACKAGE__->register_method(
1828     method    => "biblio_search_isbn",
1829     api_name  => "open-ils.search.biblio.isbn",
1830     signature => {
1831         desc   => 'Retrieve biblio IDs for a given ISBN',
1832         params => [
1833             {desc => 'ISBN', type => 'string'}  # or number maybe?  How normalized is our storage data?
1834         ],
1835         return => {
1836             desc => 'Results object like: { "count": $i, "ids": [...] }',
1837             type => 'object'
1838         }
1839     }
1840 );
1841
1842 sub biblio_search_isbn { 
1843         my( $self, $client, $isbn ) = @_;
1844         $logger->debug("Searching ISBN $isbn");
1845         my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
1846         return { ids => $recs, count => scalar(@$recs) };
1847 }
1848
1849 __PACKAGE__->register_method(
1850     method   => "biblio_search_isbn_batch",
1851     api_name => "open-ils.search.biblio.isbn_list",
1852 );
1853
1854 sub biblio_search_isbn_batch { 
1855         my( $self, $client, $isbn_list ) = @_;
1856         $logger->debug("Searching ISBNs @$isbn_list");
1857         my @recs = (); my %rec_set = ();
1858         foreach my $isbn ( @$isbn_list ) {
1859                 foreach my $rec ( @{ $U->storagereq(
1860                         'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
1861                 } ) {
1862                         if (! $rec_set{ $rec }) {
1863                                 $rec_set{ $rec } = 1;
1864                                 push @recs, $rec;
1865                         }
1866                 }
1867         }
1868         return { ids => \@recs, count => scalar(@recs) };
1869 }
1870
1871 __PACKAGE__->register_method(
1872     method   => "biblio_search_issn",
1873     api_name => "open-ils.search.biblio.issn",
1874     signature => {
1875         desc   => 'Retrieve biblio IDs for a given ISSN',
1876         params => [
1877             {desc => 'ISBN', type => 'string'}
1878         ],
1879         return => {
1880             desc => 'Results object like: { "count": $i, "ids": [...] }',
1881             type => 'object'
1882         }
1883     }
1884 );
1885
1886 sub biblio_search_issn { 
1887         my( $self, $client, $issn ) = @_;
1888         $logger->debug("Searching ISSN $issn");
1889         my $e = new_editor();
1890         $issn =~ s/-/ /g;
1891         my $recs = $U->storagereq(
1892                 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
1893         return { ids => $recs, count => scalar(@$recs) };
1894 }
1895
1896
1897 __PACKAGE__->register_method(
1898     method    => "fetch_mods_by_copy",
1899     api_name  => "open-ils.search.biblio.mods_from_copy",
1900     argc      => 1,
1901     signature => {
1902         desc    => 'Retrieve MODS record given an attached copy ID',
1903         params  => [
1904             { desc => 'Copy ID', type => 'number' }
1905         ],
1906         returns => {
1907             desc => 'MODS record, event on error or uncataloged item'
1908         }
1909     }
1910 );
1911
1912 sub fetch_mods_by_copy {
1913         my( $self, $client, $copyid ) = @_;
1914         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
1915         return $evt if $evt;
1916         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
1917         return $apputils->record_to_mvr($record);
1918 }
1919
1920
1921 # -------------------------------------------------------------------------------------
1922
1923 __PACKAGE__->register_method(
1924     method   => "cn_browse",
1925     api_name => "open-ils.search.callnumber.browse.target",
1926     notes    => "Starts a callnumber browse"
1927 );
1928
1929 __PACKAGE__->register_method(
1930     method   => "cn_browse",
1931     api_name => "open-ils.search.callnumber.browse.page_up",
1932     notes    => "Returns the previous page of callnumbers",
1933 );
1934
1935 __PACKAGE__->register_method(
1936     method   => "cn_browse",
1937     api_name => "open-ils.search.callnumber.browse.page_down",
1938     notes    => "Returns the next page of callnumbers",
1939 );
1940
1941
1942 # RETURNS array of arrays like so: label, owning_lib, record, id
1943 sub cn_browse {
1944         my( $self, $client, @params ) = @_;
1945         my $method;
1946
1947         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
1948                 if( $self->api_name =~ /target/ );
1949         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
1950                 if( $self->api_name =~ /page_up/ );
1951         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
1952                 if( $self->api_name =~ /page_down/ );
1953
1954         return $apputils->simplereq( 'open-ils.storage', $method, @params );
1955 }
1956 # -------------------------------------------------------------------------------------
1957
1958 __PACKAGE__->register_method(
1959     method        => "fetch_cn",
1960     api_name      => "open-ils.search.callnumber.retrieve",
1961     authoritative => 1,
1962     notes         => "retrieves a callnumber based on ID",
1963 );
1964
1965 sub fetch_cn {
1966         my( $self, $client, $id ) = @_;
1967         my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
1968         return $evt if $evt;
1969         return $cn;
1970 }
1971
1972 __PACKAGE__->register_method(
1973     method    => "fetch_copy_by_cn",
1974     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
1975     signature => q/
1976                 Returns an array of copy ID's by callnumber ID
1977                 @param cnid The callnumber ID
1978                 @return An array of copy IDs
1979         /
1980 );
1981
1982 sub fetch_copy_by_cn {
1983         my( $self, $conn, $cnid ) = @_;
1984         return $U->cstorereq(
1985                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
1986                 { call_number => $cnid, deleted => 'f' } );
1987 }
1988
1989 __PACKAGE__->register_method(
1990     method    => 'fetch_cn_by_info',
1991     api_name  => 'open-ils.search.call_number.retrieve_by_info',
1992     signature => q/
1993                 @param label The callnumber label
1994                 @param record The record the cn is attached to
1995                 @param org The owning library of the cn
1996                 @return The callnumber object
1997         /
1998 );
1999
2000
2001 sub fetch_cn_by_info {
2002         my( $self, $conn, $label, $record, $org ) = @_;
2003         return $U->cstorereq(
2004                 'open-ils.cstore.direct.asset.call_number.search',
2005                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2006 }
2007
2008
2009
2010 __PACKAGE__->register_method(
2011     method   => 'bib_extras',
2012     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2013 );
2014 __PACKAGE__->register_method(
2015     method   => 'bib_extras',
2016     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2017 );
2018 __PACKAGE__->register_method(
2019     method   => 'bib_extras',
2020     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2021 );
2022 __PACKAGE__->register_method(
2023     method   => 'bib_extras',
2024     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2025 );
2026 __PACKAGE__->register_method(
2027     method   => 'bib_extras',
2028     api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2029 );
2030
2031 sub bib_extras {
2032         my $self = shift;
2033
2034         my $e = new_editor();
2035
2036         return $e->retrieve_all_config_lit_form_map()
2037                 if( $self->api_name =~ /lit_form/ );
2038
2039         return $e->retrieve_all_config_item_form_map()
2040                 if( $self->api_name =~ /item_form_map/ );
2041
2042         return $e->retrieve_all_config_item_type_map()
2043                 if( $self->api_name =~ /item_type_map/ );
2044
2045         return $e->retrieve_all_config_bib_level_map()
2046                 if( $self->api_name =~ /bib_level_map/ );
2047
2048         return $e->retrieve_all_config_audience_map()
2049                 if( $self->api_name =~ /audience_map/ );
2050
2051         return [];
2052 }
2053
2054
2055
2056 __PACKAGE__->register_method(
2057     method    => 'fetch_slim_record',
2058     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2059     signature => {
2060         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2061         params => [
2062             { desc => 'Array of Record IDs', type => 'array' }
2063         ],
2064         return => { 
2065             desc => 'Array of biblio records, event on error'
2066         }
2067     }
2068 );
2069
2070 sub fetch_slim_record {
2071     my( $self, $conn, $ids ) = @_;
2072
2073 #my $editor = OpenILS::Utils::Editor->new;
2074     my $editor = new_editor();
2075         my @res;
2076     for( @$ids ) {
2077         return $editor->event unless
2078             my $r = $editor->retrieve_biblio_record_entry($_);
2079         $r->clear_marc;
2080         push(@res, $r);
2081     }
2082     return \@res;
2083 }
2084
2085
2086
2087 __PACKAGE__->register_method(
2088     method    => 'rec_to_mr_rec_descriptors',
2089     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2090     signature => q/
2091                 specialized method...
2092                 Given a biblio record id or a metarecord id, 
2093                 this returns a list of metabib.record_descriptor
2094                 objects that live within the same metarecord
2095                 @param args Object of args including:
2096         /
2097 );
2098
2099 sub rec_to_mr_rec_descriptors {
2100         my( $self, $conn, $args ) = @_;
2101
2102     my $rec        = $$args{record};
2103     my $mrec       = $$args{metarecord};
2104     my $item_forms = $$args{item_forms};
2105     my $item_types = $$args{item_types};
2106     my $item_lang  = $$args{item_lang};
2107
2108         my $e = new_editor();
2109         my $recs;
2110
2111         if( !$mrec ) {
2112                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2113                 return $e->event unless @$map;
2114                 $mrec = $$map[0]->metarecord;
2115         }
2116
2117         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2118         return $e->event unless @$recs;
2119
2120         my @recs = map { $_->source } @$recs;
2121         my $search = { record => \@recs };
2122         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2123         $search->{item_type} = $item_types if $item_types and @$item_types;
2124         $search->{item_lang} = $item_lang  if $item_lang;
2125
2126         my $desc = $e->search_metabib_record_descriptor($search);
2127
2128         return { metarecord => $mrec, descriptors => $desc };
2129 }
2130
2131
2132 __PACKAGE__->register_method(
2133     method   => 'fetch_age_protect',
2134     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2135 );
2136
2137 sub fetch_age_protect {
2138         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2139 }
2140
2141
2142 __PACKAGE__->register_method(
2143     method   => 'copies_by_cn_label',
2144     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2145 );
2146
2147 __PACKAGE__->register_method(
2148     method   => 'copies_by_cn_label',
2149     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2150 );
2151
2152 sub copies_by_cn_label {
2153         my( $self, $conn, $record, $label, $circ_lib ) = @_;
2154         my $e = new_editor();
2155         my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2156         return [] unless @$cns;
2157
2158         # show all non-deleted copies in the staff client ...
2159         if ($self->api_name =~ /staff$/o) {
2160                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2161         }
2162
2163         # ... otherwise, grab the copies ...
2164         my $copies = $e->search_asset_copy(
2165                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2166                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2167                 ]
2168         );
2169
2170         # ... and test for location and status visibility
2171         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2172 }
2173
2174
2175 1;
2176