]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Search/Biblio.pm
added ability to tag bib record results from staged multiclass search as previously...
[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 $method = ($self->api_name =~ /metabib/) ?
1088         'open-ils.storage.metabib.multiclass.staged.search_fts':
1089         'open-ils.storage.biblio.multiclass.staged.search_fts';
1090
1091     $method .= '.staff' if $self->api_name =~ /staff$/;
1092     $method .= '.atomic';
1093                 
1094     return {count => 0} unless (
1095         $search_hash and 
1096         $search_hash->{searches} and 
1097         scalar( keys %{$search_hash->{searches}} ));
1098
1099     my $search_duration;
1100     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
1101     my $user_limit  = $search_hash->{limit}  || 10;
1102     $user_offset = ($user_offset >= 0) ? $user_offset :  0;
1103     $user_limit  = ($user_limit  >= 0) ? $user_limit  : 10;
1104
1105
1106     # we're grabbing results on a per-superpage basis, which means the 
1107     # limit and offset should coincide with superpage boundaries
1108     $search_hash->{offset} = 0;
1109     $search_hash->{limit} = $superpage_size;
1110
1111     # force a well-known check_limit
1112     $search_hash->{check_limit} = $superpage_size; 
1113     # restrict total tested to superpage size * number of superpages
1114     $search_hash->{core_limit}  = $superpage_size * $max_superpages;
1115
1116     # Set the configured estimation strategy, defaults to 'inclusion'.
1117         my $estimation_strategy = OpenSRF::Utils::SettingsClient
1118         ->new
1119         ->config_value(
1120             apps => 'open-ils.search', app_settings => 'estimation_strategy'
1121         ) || 'inclusion';
1122         $search_hash->{estimation_strategy} = $estimation_strategy;
1123
1124     # pull any existing results from the cache
1125     my $key = search_cache_key($method, $search_hash);
1126     my $facet_key = $key.'_facets';
1127     my $cache_data = $cache->get_cache($key) || {};
1128
1129     # keep retrieving results until we find enough to 
1130     # fulfill the user-specified limit and offset
1131     my $all_results = [];
1132     my $page; # current superpage
1133     my $est_hit_count = 0;
1134     my $current_page_summary = {};
1135     my $global_summary = {checked => 0, visible => 0, excluded => 0, deleted => 0, total => 0};
1136     my $is_real_hit_count = 0;
1137     my $new_ids = [];
1138
1139     for($page = 0; $page < $max_superpages; $page++) {
1140
1141         my $data = $cache_data->{$page};
1142         my $results;
1143         my $summary;
1144
1145         $logger->debug("staged search: analyzing superpage $page");
1146
1147         if($data) {
1148             # this window of results is already cached
1149             $logger->debug("staged search: found cached results");
1150             $summary = $data->{summary};
1151             $results = $data->{results};
1152
1153         } else {
1154             # retrieve the window of results from the database
1155             $logger->debug("staged search: fetching results from the database");
1156             $search_hash->{skip_check} = $page * $superpage_size;
1157             my $start = time;
1158             $results = $U->storagereq($method, %$search_hash);
1159             $search_duration = time - $start;
1160             $logger->info("staged search: DB call took $search_duration seconds and returned ".scalar(@$results)." rows, including summary");
1161             $summary = shift(@$results) if $results;
1162
1163             unless($summary) {
1164                 $logger->info("search timed out: duration=$search_duration: params=".
1165                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1166                 return {count => 0};
1167             }
1168
1169             my $hc = $summary->{estimated_hit_count} || $summary->{visible};
1170             if($hc == 0) {
1171                 $logger->info("search returned 0 results: duration=$search_duration: params=".
1172                     OpenSRF::Utils::JSON->perl2JSON($search_hash));
1173             }
1174
1175             # Create backwards-compatible result structures
1176             if($self->api_name =~ /biblio/) {
1177                 $results = [map {[$_->{id}]} @$results];
1178
1179                 tag_circulated_records($search_hash->{authtoken}, $results) 
1180                     if $search_hash->{tag_circulated_records} and $search_hash->{authtoken};
1181
1182             } else {
1183                 $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
1184             }
1185
1186             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
1187             $results = [grep {defined $_->[0]} @$results];
1188             cache_staged_search_page($key, $page, $summary, $results) if $docache;
1189         }
1190
1191         $current_page_summary = $summary;
1192
1193         # add the new set of results to the set under construction
1194         push(@$all_results, @$results);
1195
1196         my $current_count = scalar(@$all_results);
1197
1198         $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
1199             if $page == 0;
1200
1201         $logger->debug("staged search: located $current_count, with estimated hits=".
1202             $summary->{estimated_hit_count}." : visible=".$summary->{visible}.", checked=".$summary->{checked});
1203
1204                 if (defined($summary->{estimated_hit_count})) {
1205             foreach (qw/ checked visible excluded deleted /) {
1206                 $global_summary->{$_} += $summary->{$_};
1207             }
1208                         $global_summary->{total} = $summary->{total};
1209                 }
1210
1211         # we've found all the possible hits
1212         last if $current_count == $summary->{visible}
1213             and not defined $summary->{estimated_hit_count};
1214
1215         # we've found enough results to satisfy the requested limit/offset
1216         last if $current_count >= ($user_limit + $user_offset);
1217
1218         # we've scanned all possible hits
1219         if($summary->{checked} < $superpage_size) {
1220             $est_hit_count = scalar(@$all_results);
1221             # we have all possible results in hand, so we know the final hit count
1222             $is_real_hit_count = 1;
1223             last;
1224         }
1225     }
1226
1227     my @results = grep {defined $_} @$all_results[$user_offset..($user_offset + $user_limit - 1)];
1228
1229         # refine the estimate if we have more than one superpage
1230         if ($page > 0 and not $is_real_hit_count) {
1231                 if ($global_summary->{checked} >= $global_summary->{total}) {
1232                         $est_hit_count = $global_summary->{visible};
1233                 } else {
1234                         my $updated_hit_count = $U->storagereq(
1235                                 'open-ils.storage.fts_paging_estimate',
1236                                 $global_summary->{checked},
1237                                 $global_summary->{visible},
1238                                 $global_summary->{excluded},
1239                                 $global_summary->{deleted},
1240                                 $global_summary->{total}
1241                         );
1242                         $est_hit_count = $updated_hit_count->{$estimation_strategy};
1243                 }
1244         }
1245
1246     $conn->respond_complete(
1247         {
1248             count             => $est_hit_count,
1249             core_limit        => $search_hash->{core_limit},
1250             superpage_size    => $search_hash->{check_limit},
1251             superpage_summary => $current_page_summary,
1252             facet_key         => $facet_key,
1253             ids               => \@results
1254         }
1255     );
1256
1257     cache_facets($facet_key, $new_ids, ($self->api_name =~ /metabib/) ? 1 : 0) if $docache;
1258
1259     return undef;
1260 }
1261
1262 sub tag_circulated_records {
1263     my ($auth, $results) = @_;
1264     my $e = new_editor(authtoken => $auth);
1265     return $results unless $e->checkauth;
1266
1267     # Give me the distinct set of bib records that exist in the user's visible circulation history
1268     my $circ_recs = $e->json_query({
1269         select => {acn => [{column => 'record', transform => 'distinct'}]}, 
1270         from => {acp => 'acn'}, 
1271         where => {
1272             '+acp' => {
1273                 id => {
1274                     in => {
1275                         from => ['action.usr_visible_circ_copies', $e->requestor->id]}
1276                 }
1277             }
1278         }
1279     });
1280
1281     # if the record appears in the circ history, push a 1 onto 
1282     # the rec array structure to indicate truthiness
1283     for my $rec (@$results) {
1284         push(@$rec, 1) if grep { $_->{record} eq $$rec[0] } @$circ_recs;
1285     }
1286
1287     $results
1288 }
1289
1290 # creates a unique token to represent the query in the cache
1291 sub search_cache_key {
1292     my $method = shift;
1293     my $search_hash = shift;
1294         my @sorted;
1295     for my $key (sort keys %$search_hash) {
1296             push(@sorted, ($key => $$search_hash{$key})) 
1297             unless $key eq 'limit'  or 
1298                    $key eq 'offset' or 
1299                    $key eq 'skip_check';
1300     }
1301         my $s = OpenSRF::Utils::JSON->perl2JSON(\@sorted);
1302         return $pfx . md5_hex($method . $s);
1303 }
1304
1305 sub retrieve_cached_facets {
1306     my $self   = shift;
1307     my $client = shift;
1308     my $key    = shift;
1309
1310     return undef unless ($key and $key =~ /_facets$/);
1311
1312     return $cache->get_cache($key) || {};
1313 }
1314
1315 __PACKAGE__->register_method(
1316     method   => "retrieve_cached_facets",
1317     api_name => "open-ils.search.facet_cache.retrieve"
1318 );
1319
1320
1321 sub cache_facets {
1322     # add facets for this search to the facet cache
1323     my($key, $results, $metabib) = @_;
1324     my $data = $cache->get_cache($key);
1325     $data ||= {};
1326
1327     return undef unless (@$results);
1328
1329     # The query we're constructing
1330     #
1331     # select  cmf.id,
1332     #         mfae.value,
1333     #         count(distinct mmrsm.appropriate-id-field )
1334     #   from  metabib.facet_entry mfae
1335     #         join config.metabib_field cmf on (mfae.field = cmf.id)
1336     #         join metabib.metarecord_sourc_map mmrsm on (mfae.source = mmrsm.source)
1337     #   where cmf.facet_field
1338     #         and mmrsm.appropriate-id-field in IDLIST
1339     #   group by 1,2;
1340
1341     my $count_field = $metabib ? 'metarecord' : 'source';
1342     my $facets = $U->cstorereq( "open-ils.cstore.json_query.atomic",
1343         {   select  => {
1344                 cmf  => [ 'id' ],
1345                 mfae => [ 'value' ],
1346                 mmrsm => [{
1347                     transform => 'count',
1348                     distinct => 1,
1349                     column => $count_field,
1350                     alias => 'count',
1351                     aggregate => 1
1352                 }]
1353             },
1354             from    => {
1355                 mfae => {
1356                     cmf   => { field => 'id',     fkey => 'field'  },
1357                     mmrsm => { field => 'source', fkey => 'source' }
1358                 }
1359             },
1360             where   => {
1361                 '+cmf'   => 'facet_field',
1362                 '+mmrsm' => { $count_field => $results }
1363             }
1364         }
1365     );
1366
1367     for my $facet (@$facets) {
1368         next unless ($facet->{value});
1369         $data->{$facet->{id}}->{$facet->{value}} += $facet->{count};
1370     }
1371
1372     $logger->info("facet compilation: cached with key=$key");
1373
1374     $cache->put_cache($key, $data, $cache_timeout);
1375 }
1376
1377 sub cache_staged_search_page {
1378     # puts this set of results into the cache
1379     my($key, $page, $summary, $results) = @_;
1380     my $data = $cache->get_cache($key);
1381     $data ||= {};
1382     $data->{$page} = {
1383         summary => $summary,
1384         results => $results
1385     };
1386
1387     $logger->info("staged search: cached with key=$key, superpage=$page, estimated=".
1388         $summary->{estimated_hit_count}.", visible=".$summary->{visible});
1389
1390     $cache->put_cache($key, $data, $cache_timeout);
1391 }
1392
1393 sub search_cache {
1394
1395         my $key         = shift;
1396         my $offset      = shift;
1397         my $limit       = shift;
1398         my $start       = $offset;
1399         my $end         = $offset + $limit - 1;
1400
1401         $logger->debug("searching cache for $key : $start..$end\n");
1402
1403         return undef unless $cache;
1404         my $data = $cache->get_cache($key);
1405
1406         return undef unless $data;
1407
1408         my $count = $data->[0];
1409         $data = $data->[1];
1410
1411         return undef unless $offset < $count;
1412
1413         my @result;
1414         for( my $i = $offset; $i <= $end; $i++ ) {
1415                 last unless my $d = $$data[$i];
1416                 push( @result, $d );
1417         }
1418
1419         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
1420
1421         return \@result;
1422 }
1423
1424
1425 sub put_cache {
1426         my( $key, $count, $data ) = @_;
1427         return undef unless $cache;
1428         $logger->debug("search_cache putting ".
1429                 scalar(@$data)." items at key $key with timeout $cache_timeout");
1430         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
1431 }
1432
1433
1434 __PACKAGE__->register_method(
1435     method   => "biblio_mrid_to_modsbatch_batch",
1436     api_name => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve"
1437 );
1438
1439 sub biblio_mrid_to_modsbatch_batch {
1440         my( $self, $client, $mrids) = @_;
1441         # warn "Performing mrid_to_modsbatch_batch..."; # unconditional warn
1442         my @mods;
1443         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
1444         for my $id (@$mrids) {
1445                 next unless defined $id;
1446                 my ($m) = $method->run($id);
1447                 push @mods, $m;
1448         }
1449         return \@mods;
1450 }
1451
1452
1453 foreach (qw /open-ils.search.biblio.metarecord.mods_slim.retrieve
1454              open-ils.search.biblio.metarecord.mods_slim.retrieve.staff/)
1455     {
1456     __PACKAGE__->register_method(
1457         method    => "biblio_mrid_to_modsbatch",
1458         api_name  => $_,
1459         signature => {
1460             desc   => "Returns the mvr associated with a given metarecod. If none exists, it is created.  "
1461                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1462             params => [
1463                 { desc => 'Metarecord ID', type => 'number' },
1464                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1465             ],
1466             return => {
1467                 desc => 'MVR Object, event on error',
1468             }
1469         }
1470     );
1471 }
1472
1473 sub biblio_mrid_to_modsbatch {
1474         my( $self, $client, $mrid, $args) = @_;
1475
1476         # warn "Grabbing mvr for $mrid\n";    # unconditional warn
1477
1478         my ($mr, $evt) = _grab_metarecord($mrid);
1479         return $evt unless $mr;
1480
1481         my $mvr = biblio_mrid_check_mvr($self, $client, $mr) ||
1482               biblio_mrid_make_modsbatch($self, $client, $mr);
1483
1484         return $mvr unless ref($args);  
1485
1486         # Here we find the lead record appropriate for the given filters 
1487         # and use that for the title and author of the metarecord
1488     my $format = $$args{format};
1489     my $org    = $$args{org};
1490     my $depth  = $$args{depth};
1491
1492         return $mvr unless $format or $org or $depth;
1493
1494         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
1495         $method = "$method.staff" if $self->api_name =~ /staff/o; 
1496
1497         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
1498
1499         if( my $mods = $U->record_to_mvr($rec) ) {
1500
1501         $mvr->title( $mods->title );
1502         $mvr->author($mods->author);
1503                 $logger->debug("mods_slim updating title and ".
1504                         "author in mvr with ".$mods->title." : ".$mods->author);
1505         }
1506
1507         return $mvr;
1508 }
1509
1510 # converts a metarecord to an mvr
1511 sub _mr_to_mvr {
1512         my $mr = shift;
1513         my $perl = OpenSRF::Utils::JSON->JSON2perl($mr->mods());
1514         return Fieldmapper::metabib::virtual_record->new($perl);
1515 }
1516
1517 # checks to see if a metarecord has mods, if so returns true;
1518
1519 __PACKAGE__->register_method(
1520     method   => "biblio_mrid_check_mvr",
1521     api_name => "open-ils.search.biblio.metarecord.mods_slim.check",
1522     notes    => "Takes a metarecord ID or a metarecord object and returns true "
1523               . "if the metarecord already has an mvr associated with it."
1524 );
1525
1526 sub biblio_mrid_check_mvr {
1527         my( $self, $client, $mrid ) = @_;
1528         my $mr; 
1529
1530         my $evt;
1531         if(ref($mrid)) { $mr = $mrid; } 
1532         else { ($mr, $evt) = _grab_metarecord($mrid); }
1533         return $evt if $evt;
1534
1535         # warn "Checking mvr for mr " . $mr->id . "\n";   # unconditional warn
1536
1537         return _mr_to_mvr($mr) if $mr->mods();
1538         return undef;
1539 }
1540
1541 sub _grab_metarecord {
1542         my $mrid = shift;
1543         #my $e = OpenILS::Utils::Editor->new;
1544         my $e = new_editor();
1545         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
1546         return ($mr);
1547 }
1548
1549
1550 __PACKAGE__->register_method(
1551     method   => "biblio_mrid_make_modsbatch",
1552     api_name => "open-ils.search.biblio.metarecord.mods_slim.create",
1553     notes    => "Takes either a metarecord ID or a metarecord object. "
1554               . "Forces the creations of an mvr for the given metarecord. "
1555               . "The created mvr is returned."
1556 );
1557
1558 sub biblio_mrid_make_modsbatch {
1559         my( $self, $client, $mrid ) = @_;
1560
1561         #my $e = OpenILS::Utils::Editor->new;
1562         my $e = new_editor();
1563
1564         my $mr;
1565         if( ref($mrid) ) {
1566                 $mr = $mrid;
1567                 $mrid = $mr->id;
1568         } else {
1569                 $mr = $e->retrieve_metabib_metarecord($mrid) 
1570                         or return $e->event;
1571         }
1572
1573         my $masterid = $mr->master_record;
1574         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
1575
1576         my $ids = $U->storagereq(
1577                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
1578         return undef unless @$ids;
1579
1580         my $master = $e->retrieve_biblio_record_entry($masterid)
1581                 or return $e->event;
1582
1583         # start the mods batch
1584         my $u = OpenILS::Utils::ModsParser->new();
1585         $u->start_mods_batch( $master->marc );
1586
1587         # grab all of the sub-records and shove them into the batch
1588         my @ids = grep { $_ ne $masterid } @$ids;
1589         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
1590
1591         my $subrecs = [];
1592         if(@$ids) {
1593                 for my $i (@$ids) {
1594                         my $r = $e->retrieve_biblio_record_entry($i);
1595                         push( @$subrecs, $r ) if $r;
1596                 }
1597         }
1598
1599         for(@$subrecs) {
1600                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
1601                 $u->push_mods_batch( $_->marc ) if $_->marc;
1602         }
1603
1604
1605         # finish up and send to the client
1606         my $mods = $u->finish_mods_batch();
1607         $mods->doc_id($mrid);
1608         $client->respond_complete($mods);
1609
1610
1611         # now update the mods string in the db
1612         my $string = OpenSRF::Utils::JSON->perl2JSON($mods->decast);
1613         $mr->mods($string);
1614
1615         #$e = OpenILS::Utils::Editor->new(xact => 1);
1616         $e = new_editor(xact => 1);
1617         $e->update_metabib_metarecord($mr) 
1618                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
1619         $e->finish;
1620
1621         return undef;
1622 }
1623
1624
1625 # converts a mr id into a list of record ids
1626
1627 foreach (qw/open-ils.search.biblio.metarecord_to_records
1628             open-ils.search.biblio.metarecord_to_records.staff/)
1629 {
1630     __PACKAGE__->register_method(
1631         method    => "biblio_mrid_to_record_ids",
1632         api_name  => $_,
1633         signature => {
1634             desc   => "Fetch record IDs corresponding to a meta-record ID, with optional search filters. "
1635                     . "As usual, the .staff version of this method will include otherwise hidden records.",
1636             params => [
1637                 { desc => 'Metarecord ID', type => 'number' },
1638                 { desc => '(Optional) Search filters hash with possible keys: format, org, depth', type => 'object' }
1639             ],
1640             return => {
1641                 desc => 'Results object like {count => $i, ids =>[...]}',
1642                 type => 'object'
1643             }
1644             
1645         }
1646     );
1647 }
1648
1649 sub biblio_mrid_to_record_ids {
1650         my( $self, $client, $mrid, $args ) = @_;
1651
1652     my $format = $$args{format};
1653     my $org    = $$args{org};
1654     my $depth  = $$args{depth};
1655
1656         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
1657         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
1658         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
1659
1660         return { count => scalar(@$recs), ids => $recs };
1661 }
1662
1663
1664 __PACKAGE__->register_method(
1665     method   => "biblio_record_to_marc_html",
1666     api_name => "open-ils.search.biblio.record.html"
1667 );
1668
1669 __PACKAGE__->register_method(
1670     method   => "biblio_record_to_marc_html",
1671     api_name => "open-ils.search.authority.to_html"
1672 );
1673
1674 # Persistent parsers and setting objects
1675 my $parser = XML::LibXML->new();
1676 my $xslt   = XML::LibXSLT->new();
1677 my $marc_sheet;
1678 my $slim_marc_sheet;
1679 my $settings_client = OpenSRF::Utils::SettingsClient->new();
1680
1681 sub biblio_record_to_marc_html {
1682         my($self, $client, $recordid, $slim, $marcxml) = @_;
1683
1684     my $sheet;
1685         my $dir = $settings_client->config_value("dirs", "xsl");
1686
1687     if($slim) {
1688         unless($slim_marc_sheet) {
1689                     my $xsl = $settings_client->config_value(
1690                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl_slim');
1691             if($xsl) {
1692                         $xsl = $parser->parse_file("$dir/$xsl");
1693                         $slim_marc_sheet = $xslt->parse_stylesheet($xsl);
1694             }
1695         }
1696         $sheet = $slim_marc_sheet;
1697     }
1698
1699     unless($sheet) {
1700         unless($marc_sheet) {
1701             my $xsl_key = ($slim) ? 'marc_html_xsl_slim' : 'marc_html_xsl';
1702                     my $xsl = $settings_client->config_value(
1703                             "apps", "open-ils.search", "app_settings", 'marc_html_xsl');
1704                     $xsl = $parser->parse_file("$dir/$xsl");
1705                     $marc_sheet = $xslt->parse_stylesheet($xsl);
1706         }
1707         $sheet = $marc_sheet;
1708     }
1709
1710     my $record;
1711     unless($marcxml) {
1712         my $e = new_editor();
1713         if($self->api_name =~ /authority/) {
1714             $record = $e->retrieve_authority_record_entry($recordid)
1715                 or return $e->event;
1716         } else {
1717             $record = $e->retrieve_biblio_record_entry($recordid)
1718                 or return $e->event;
1719         }
1720         $marcxml = $record->marc;
1721     }
1722
1723         my $xmldoc = $parser->parse_string($marcxml);
1724         my $html = $sheet->transform($xmldoc);
1725         return $html->documentElement->toString();
1726 }
1727
1728 __PACKAGE__->register_method(
1729     method    => "format_biblio_record_entry",
1730     api_name  => "open-ils.search.biblio.record.print",
1731     signature => {
1732         desc   => 'Returns a printable version of the specified bib record',
1733         params => [
1734             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1735         ],
1736         return => {
1737             desc => q/An action_trigger.event object or error event./,
1738             type => 'object',
1739         }
1740     }
1741 );
1742 __PACKAGE__->register_method(
1743     method    => "format_biblio_record_entry",
1744     api_name  => "open-ils.search.biblio.record.email",
1745     signature => {
1746         desc   => 'Emails an A/T templated version of the specified bib records to the authorized user',
1747         params => [
1748             { desc => 'Authentication token',  type => 'string'},
1749             { desc => 'Biblio record entry ID or array of IDs', type => 'number' },
1750         ],
1751         return => {
1752             desc => q/Undefined on success, otherwise an error event./,
1753             type => 'object',
1754         }
1755     }
1756 );
1757
1758 sub format_biblio_record_entry {
1759     my($self, $conn, $arg1, $arg2) = @_;
1760
1761     my $for_print = ($self->api_name =~ /print/);
1762     my $for_email = ($self->api_name =~ /email/);
1763
1764     my $e; my $auth; my $bib_id; my $context_org;
1765
1766     if ($for_print) {
1767         $bib_id = $arg1;
1768         $context_org = $arg2 || $U->fetch_org_tree->id;
1769         $e = new_editor(xact => 1);
1770     } elsif ($for_email) {
1771         $auth = $arg1;
1772         $bib_id = $arg2;
1773         $e = new_editor(authtoken => $auth, xact => 1);
1774         return $e->die_event unless $e->checkauth;
1775         $context_org = $e->requestor->home_ou;
1776     }
1777
1778     my $bib_ids;
1779     if (ref $bib_id ne 'ARRAY') {
1780         $bib_ids = [ $bib_id ];
1781     } else {
1782         $bib_ids = $bib_id;
1783     }
1784
1785     my $bucket = Fieldmapper::container::biblio_record_entry_bucket->new;
1786     $bucket->btype('temp');
1787     $bucket->name('format_biblio_record_entry ' . $U->create_uuid_string);
1788     if ($for_email) {
1789         $bucket->owner($e->requestor) 
1790     } else {
1791         $bucket->owner(1);
1792     }
1793     my $bucket_obj = $e->create_container_biblio_record_entry_bucket($bucket);
1794
1795     for my $id (@$bib_ids) {
1796
1797         my $bib = $e->retrieve_biblio_record_entry([$id]) or return $e->die_event;
1798
1799         my $bucket_entry = Fieldmapper::container::biblio_record_entry_bucket_item->new;
1800         $bucket_entry->target_biblio_record_entry($bib);
1801         $bucket_entry->bucket($bucket_obj->id);
1802         $e->create_container_biblio_record_entry_bucket_item($bucket_entry);
1803     }
1804
1805     $e->commit;
1806
1807     if ($for_print) {
1808
1809         return $U->fire_object_event(undef, 'biblio.format.record_entry.print', [ $bucket ], $context_org);
1810
1811     } elsif ($for_email) {
1812
1813         $U->create_events_for_hook('biblio.format.record_entry.email', $bucket, $context_org, undef, undef, 1);
1814     }
1815
1816     return undef;
1817 }
1818
1819
1820 __PACKAGE__->register_method(
1821     method   => "retrieve_all_copy_statuses",
1822     api_name => "open-ils.search.config.copy_status.retrieve.all"
1823 );
1824
1825 sub retrieve_all_copy_statuses {
1826         my( $self, $client ) = @_;
1827         return new_editor()->retrieve_all_config_copy_status();
1828 }
1829
1830
1831 __PACKAGE__->register_method(
1832     method   => "copy_counts_per_org",
1833     api_name => "open-ils.search.biblio.copy_counts.retrieve"
1834 );
1835
1836 __PACKAGE__->register_method(
1837     method   => "copy_counts_per_org",
1838     api_name => "open-ils.search.biblio.copy_counts.retrieve.staff"
1839 );
1840
1841 sub copy_counts_per_org {
1842         my( $self, $client, $record_id ) = @_;
1843
1844         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
1845
1846         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
1847         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
1848
1849         my $counts = $apputils->simple_scalar_request(
1850                 "open-ils.storage", $method, $record_id );
1851
1852         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
1853         return $counts;
1854 }
1855
1856
1857 __PACKAGE__->register_method(
1858     method   => "copy_count_summary",
1859     api_name => "open-ils.search.biblio.copy_counts.summary.retrieve",
1860     notes    => "returns an array of these: "
1861               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1862               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1863 );
1864                 
1865
1866 sub copy_count_summary {
1867         my( $self, $client, $rid, $org, $depth ) = @_;
1868     $org   ||= 1;
1869     $depth ||= 0;
1870     my $data = $U->storagereq(
1871                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
1872
1873     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1874 }
1875
1876 __PACKAGE__->register_method(
1877     method   => "copy_location_count_summary",
1878     api_name => "open-ils.search.biblio.copy_location_counts.summary.retrieve",
1879     notes    => "returns an array of these: "
1880               . "[ org_id, callnumber_label, copy_location, <status1_count>, <status2_count>,...] "
1881               . "where statusx is a copy status name.  The statuses are sorted by ID.",
1882 );
1883
1884 sub copy_location_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_location_count.atomic', $rid, $org, $depth );
1890
1891     return [ sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @$data ];
1892 }
1893
1894 __PACKAGE__->register_method(
1895     method   => "copy_count_location_summary",
1896     api_name => "open-ils.search.biblio.copy_counts.location.summary.retrieve",
1897     notes    => "returns an array of these: "
1898               . "[ org_id, callnumber_label, <status1_count>, <status2_count>,...] "
1899               . "where statusx is a copy status name.  The statuses are sorted by ID."
1900 );
1901
1902 sub copy_count_location_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     return [ sort { $a->[1] cmp $b->[1] } @$data ];
1909 }
1910
1911
1912 foreach (qw/open-ils.search.biblio.marc
1913             open-ils.search.biblio.marc.staff/)
1914 {
1915 __PACKAGE__->register_method(
1916     method    => "marc_search",
1917     api_name  => $_,
1918     signature => {
1919         desc   => 'Fetch biblio IDs based on MARC record criteria.  '
1920                 . 'As usual, the .staff version of the search includes otherwise hidden records',
1921         params => [
1922             {
1923                 desc => 'Search hash (required) with possible elements: searches, limit, offset, sort, sort_dir. ' .
1924                         'See perldoc ' . __PACKAGE__ . ' for more detail.',
1925                 type => 'object'
1926             },
1927             {desc => 'limit (optional)',  type => 'number'},
1928             {desc => 'offset (optional)', type => 'number'}
1929         ],
1930         return => {
1931             desc => 'Results object like: { "count": $i, "ids": [...] }',
1932             type => 'object'
1933         }
1934     }
1935 );
1936 }
1937
1938 =head3 open-ils.search.biblio.marc (arghash, limit, offset)
1939
1940 As elsewhere the arghash is the required argument, and must be a hashref.  The keys are:
1941
1942     searches: complex query object  (required)
1943     org_unit: The org ID to focus the search at
1944     depth   : The org depth     
1945     limit   : integer search limit      default: 10
1946     offset  : integer search offset     default:  0
1947     sort    : What field to sort the results on? [ author | title | pubdate ]
1948     sort_dir: In what direction do we sort? [ asc | desc ]
1949
1950 Additional keys to refine search criteria:
1951
1952     audience : Audience
1953     language : Language (code)
1954     lit_form : Literary form
1955     item_form: Item form
1956     item_type: Item type
1957     format   : The MARC format
1958
1959 Please note that the specific strings to be used in the "addtional keys" will be entirely
1960 dependent on your loaded data.  
1961
1962 All keys except "searches" are optional.
1963 The "searches" value must be an arrayref of hashref elements, including keys "term" and "restrict".  
1964
1965 For example, an arg hash might look like:
1966
1967     $arghash = {
1968         searches => [
1969             {
1970                 term     => "harry",
1971                 restrict => [
1972                     {
1973                         tag => 245,
1974                         subfield => "a"
1975                     }
1976                     # ...
1977                 ]
1978             }
1979             # ...
1980         ],
1981         org_unit  => 1,
1982         limit     => 5,
1983         sort      => "author",
1984         item_type => "g"
1985     }
1986
1987 The arghash is eventually passed to the SRF call:
1988 L<open-ils.storage.biblio.full_rec.multi_search[.staff].atomic>
1989
1990 Presently, search uses the cache unconditionally.
1991
1992 =cut
1993
1994 # FIXME: that example above isn't actually tested.
1995 # TODO: docache option?
1996 sub marc_search {
1997         my( $self, $conn, $args, $limit, $offset ) = @_;
1998
1999         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
2000         $method .= ".staff" if $self->api_name =~ /staff/;
2001         $method .= ".atomic";
2002
2003     $limit  ||= 10;     # FIXME: what about $args->{limit} ?
2004     $offset ||=  0;     # FIXME: what about $args->{offset} ?
2005
2006         my @search;
2007         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
2008         my $ckey = $pfx . md5_hex($method . OpenSRF::Utils::JSON->perl2JSON(\@search));
2009
2010         my $recs = search_cache($ckey, $offset, $limit);
2011
2012         if(!$recs) {
2013                 $recs = $U->storagereq($method, %$args) || [];
2014                 if( $recs ) {
2015                         put_cache($ckey, scalar(@$recs), $recs);
2016                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
2017                 } else {
2018                         $recs = [];
2019                 }
2020         }
2021
2022         my $count = 0;
2023         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
2024         my @recs = map { $_->[0] } @$recs;
2025
2026         return { ids => \@recs, count => $count };
2027 }
2028
2029
2030 __PACKAGE__->register_method(
2031     method    => "biblio_search_isbn",
2032     api_name  => "open-ils.search.biblio.isbn",
2033     signature => {
2034         desc   => 'Retrieve biblio IDs for a given ISBN',
2035         params => [
2036             {desc => 'ISBN', type => 'string'}  # or number maybe?  How normalized is our storage data?
2037         ],
2038         return => {
2039             desc => 'Results object like: { "count": $i, "ids": [...] }',
2040             type => 'object'
2041         }
2042     }
2043 );
2044
2045 sub biblio_search_isbn { 
2046         my( $self, $client, $isbn ) = @_;
2047         $logger->debug("Searching ISBN $isbn");
2048         my $recs = $U->storagereq('open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn);
2049         return { ids => $recs, count => scalar(@$recs) };
2050 }
2051
2052 __PACKAGE__->register_method(
2053     method   => "biblio_search_isbn_batch",
2054     api_name => "open-ils.search.biblio.isbn_list",
2055 );
2056
2057 sub biblio_search_isbn_batch { 
2058         my( $self, $client, $isbn_list ) = @_;
2059         $logger->debug("Searching ISBNs @$isbn_list");
2060         my @recs = (); my %rec_set = ();
2061         foreach my $isbn ( @$isbn_list ) {
2062                 foreach my $rec ( @{ $U->storagereq(
2063                         'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn )
2064                 } ) {
2065                         if (! $rec_set{ $rec }) {
2066                                 $rec_set{ $rec } = 1;
2067                                 push @recs, $rec;
2068                         }
2069                 }
2070         }
2071         return { ids => \@recs, count => scalar(@recs) };
2072 }
2073
2074 __PACKAGE__->register_method(
2075     method   => "biblio_search_issn",
2076     api_name => "open-ils.search.biblio.issn",
2077     signature => {
2078         desc   => 'Retrieve biblio IDs for a given ISSN',
2079         params => [
2080             {desc => 'ISBN', type => 'string'}
2081         ],
2082         return => {
2083             desc => 'Results object like: { "count": $i, "ids": [...] }',
2084             type => 'object'
2085         }
2086     }
2087 );
2088
2089 sub biblio_search_issn { 
2090         my( $self, $client, $issn ) = @_;
2091         $logger->debug("Searching ISSN $issn");
2092         my $e = new_editor();
2093         $issn =~ s/-/ /g;
2094         my $recs = $U->storagereq(
2095                 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
2096         return { ids => $recs, count => scalar(@$recs) };
2097 }
2098
2099
2100 __PACKAGE__->register_method(
2101     method    => "fetch_mods_by_copy",
2102     api_name  => "open-ils.search.biblio.mods_from_copy",
2103     argc      => 1,
2104     signature => {
2105         desc    => 'Retrieve MODS record given an attached copy ID',
2106         params  => [
2107             { desc => 'Copy ID', type => 'number' }
2108         ],
2109         returns => {
2110             desc => 'MODS record, event on error or uncataloged item'
2111         }
2112     }
2113 );
2114
2115 sub fetch_mods_by_copy {
2116         my( $self, $client, $copyid ) = @_;
2117         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2118         return $evt if $evt;
2119         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2120         return $apputils->record_to_mvr($record);
2121 }
2122
2123
2124 # -------------------------------------------------------------------------------------
2125
2126 __PACKAGE__->register_method(
2127     method   => "cn_browse",
2128     api_name => "open-ils.search.callnumber.browse.target",
2129     notes    => "Starts a callnumber browse"
2130 );
2131
2132 __PACKAGE__->register_method(
2133     method   => "cn_browse",
2134     api_name => "open-ils.search.callnumber.browse.page_up",
2135     notes    => "Returns the previous page of callnumbers",
2136 );
2137
2138 __PACKAGE__->register_method(
2139     method   => "cn_browse",
2140     api_name => "open-ils.search.callnumber.browse.page_down",
2141     notes    => "Returns the next page of callnumbers",
2142 );
2143
2144
2145 # RETURNS array of arrays like so: label, owning_lib, record, id
2146 sub cn_browse {
2147         my( $self, $client, @params ) = @_;
2148         my $method;
2149
2150         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2151                 if( $self->api_name =~ /target/ );
2152         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2153                 if( $self->api_name =~ /page_up/ );
2154         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2155                 if( $self->api_name =~ /page_down/ );
2156
2157         return $apputils->simplereq( 'open-ils.storage', $method, @params );
2158 }
2159 # -------------------------------------------------------------------------------------
2160
2161 __PACKAGE__->register_method(
2162     method        => "fetch_cn",
2163     api_name      => "open-ils.search.callnumber.retrieve",
2164     authoritative => 1,
2165     notes         => "retrieves a callnumber based on ID",
2166 );
2167
2168 sub fetch_cn {
2169         my( $self, $client, $id ) = @_;
2170         my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2171         return $evt if $evt;
2172         return $cn;
2173 }
2174
2175 __PACKAGE__->register_method(
2176     method    => "fetch_copy_by_cn",
2177     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2178     signature => q/
2179                 Returns an array of copy ID's by callnumber ID
2180                 @param cnid The callnumber ID
2181                 @return An array of copy IDs
2182         /
2183 );
2184
2185 sub fetch_copy_by_cn {
2186         my( $self, $conn, $cnid ) = @_;
2187         return $U->cstorereq(
2188                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2189                 { call_number => $cnid, deleted => 'f' } );
2190 }
2191
2192 __PACKAGE__->register_method(
2193     method    => 'fetch_cn_by_info',
2194     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2195     signature => q/
2196                 @param label The callnumber label
2197                 @param record The record the cn is attached to
2198                 @param org The owning library of the cn
2199                 @return The callnumber object
2200         /
2201 );
2202
2203
2204 sub fetch_cn_by_info {
2205         my( $self, $conn, $label, $record, $org ) = @_;
2206         return $U->cstorereq(
2207                 'open-ils.cstore.direct.asset.call_number.search',
2208                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2209 }
2210
2211
2212
2213 __PACKAGE__->register_method(
2214     method   => 'bib_extras',
2215     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all'
2216 );
2217 __PACKAGE__->register_method(
2218     method   => 'bib_extras',
2219     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all'
2220 );
2221 __PACKAGE__->register_method(
2222     method   => 'bib_extras',
2223     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all'
2224 );
2225 __PACKAGE__->register_method(
2226     method   => 'bib_extras',
2227     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all'
2228 );
2229 __PACKAGE__->register_method(
2230     method   => 'bib_extras',
2231     api_name => 'open-ils.search.biblio.audience_map.retrieve.all'
2232 );
2233
2234 sub bib_extras {
2235         my $self = shift;
2236
2237         my $e = new_editor();
2238
2239         return $e->retrieve_all_config_lit_form_map()
2240                 if( $self->api_name =~ /lit_form/ );
2241
2242         return $e->retrieve_all_config_item_form_map()
2243                 if( $self->api_name =~ /item_form_map/ );
2244
2245         return $e->retrieve_all_config_item_type_map()
2246                 if( $self->api_name =~ /item_type_map/ );
2247
2248         return $e->retrieve_all_config_bib_level_map()
2249                 if( $self->api_name =~ /bib_level_map/ );
2250
2251         return $e->retrieve_all_config_audience_map()
2252                 if( $self->api_name =~ /audience_map/ );
2253
2254         return [];
2255 }
2256
2257
2258
2259 __PACKAGE__->register_method(
2260     method    => 'fetch_slim_record',
2261     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2262     signature => {
2263         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2264         params => [
2265             { desc => 'Array of Record IDs', type => 'array' }
2266         ],
2267         return => { 
2268             desc => 'Array of biblio records, event on error'
2269         }
2270     }
2271 );
2272
2273 sub fetch_slim_record {
2274     my( $self, $conn, $ids ) = @_;
2275
2276 #my $editor = OpenILS::Utils::Editor->new;
2277     my $editor = new_editor();
2278         my @res;
2279     for( @$ids ) {
2280         return $editor->event unless
2281             my $r = $editor->retrieve_biblio_record_entry($_);
2282         $r->clear_marc;
2283         push(@res, $r);
2284     }
2285     return \@res;
2286 }
2287
2288
2289
2290 __PACKAGE__->register_method(
2291     method    => 'rec_to_mr_rec_descriptors',
2292     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2293     signature => q/
2294                 specialized method...
2295                 Given a biblio record id or a metarecord id, 
2296                 this returns a list of metabib.record_descriptor
2297                 objects that live within the same metarecord
2298                 @param args Object of args including:
2299         /
2300 );
2301
2302 sub rec_to_mr_rec_descriptors {
2303         my( $self, $conn, $args ) = @_;
2304
2305     my $rec        = $$args{record};
2306     my $mrec       = $$args{metarecord};
2307     my $item_forms = $$args{item_forms};
2308     my $item_types = $$args{item_types};
2309     my $item_lang  = $$args{item_lang};
2310
2311         my $e = new_editor();
2312         my $recs;
2313
2314         if( !$mrec ) {
2315                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2316                 return $e->event unless @$map;
2317                 $mrec = $$map[0]->metarecord;
2318         }
2319
2320         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2321         return $e->event unless @$recs;
2322
2323         my @recs = map { $_->source } @$recs;
2324         my $search = { record => \@recs };
2325         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2326         $search->{item_type} = $item_types if $item_types and @$item_types;
2327         $search->{item_lang} = $item_lang  if $item_lang;
2328
2329         my $desc = $e->search_metabib_record_descriptor($search);
2330
2331         return { metarecord => $mrec, descriptors => $desc };
2332 }
2333
2334
2335 __PACKAGE__->register_method(
2336     method   => 'fetch_age_protect',
2337     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2338 );
2339
2340 sub fetch_age_protect {
2341         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2342 }
2343
2344
2345 __PACKAGE__->register_method(
2346     method   => 'copies_by_cn_label',
2347     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2348 );
2349
2350 __PACKAGE__->register_method(
2351     method   => 'copies_by_cn_label',
2352     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2353 );
2354
2355 sub copies_by_cn_label {
2356         my( $self, $conn, $record, $label, $circ_lib ) = @_;
2357         my $e = new_editor();
2358         my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2359         return [] unless @$cns;
2360
2361         # show all non-deleted copies in the staff client ...
2362         if ($self->api_name =~ /staff$/o) {
2363                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2364         }
2365
2366         # ... otherwise, grab the copies ...
2367         my $copies = $e->search_asset_copy(
2368                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2369                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2370                 ]
2371         );
2372
2373         # ... and test for location and status visibility
2374         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2375 }
2376
2377
2378 1;
2379