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