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