]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
Merge branch 'master' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen-equin...
[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'}
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         # the previous implementation of this method was essentially unlimited,
2099         # so we will set our limit very high and let multiclass.query provide any
2100         # actual limit
2101         # XXX: if making this unlimited is deemed important, we might consider
2102         # reworking 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
2103         # which is functionally deprecated at this point, or a custom call to
2104         # 'open-ils.storage.biblio.multiclass.search_fts'
2105         my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2106         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2107         my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2108         return { ids => \@recs, count => $search_result->{'count'} };
2109 }
2110
2111 __PACKAGE__->register_method(
2112     method   => "biblio_search_isbn_batch",
2113     api_name => "open-ils.search.biblio.isbn_list",
2114 );
2115
2116 # XXX: see biblio_search_isbn() for note concerning 'limit'
2117 sub biblio_search_isbn_batch { 
2118         my( $self, $client, $isbn_list ) = @_;
2119         $logger->debug("Searching ISBNs @$isbn_list");
2120         my @recs = (); my %rec_set = ();
2121         my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2122         foreach my $isbn ( @$isbn_list ) {
2123                 my ($search_result) = $method->run({'limit' => 1000000}, "identifier|isbn:$isbn");
2124                 my @recs_subset = map { $_->[0] } @{$search_result->{'ids'}};
2125                 foreach my $rec (@recs_subset) {
2126                         if (! $rec_set{ $rec }) {
2127                                 $rec_set{ $rec } = 1;
2128                                 push @recs, $rec;
2129                         }
2130                 }
2131         }
2132         return { ids => \@recs, count => scalar(@recs) };
2133 }
2134
2135 __PACKAGE__->register_method(
2136     method   => "biblio_search_issn",
2137     api_name => "open-ils.search.biblio.issn",
2138     signature => {
2139         desc   => 'Retrieve biblio IDs for a given ISSN',
2140         params => [
2141             {desc => 'ISBN', type => 'string'}
2142         ],
2143         return => {
2144             desc => 'Results object like: { "count": $i, "ids": [...] }',
2145             type => 'object'
2146         }
2147     }
2148 );
2149
2150 sub biblio_search_issn { 
2151         my( $self, $client, $issn ) = @_;
2152         $logger->debug("Searching ISSN $issn");
2153         # the previous implementation of this method was essentially unlimited,
2154         # so we will set our limit very high and let multiclass.query provide any
2155         # actual limit
2156         # XXX: if making this unlimited is deemed important, we might consider
2157         # reworking 'open-ils.storage.id_list.biblio.record_entry.search.issn',
2158         # which is functionally deprecated at this point, or a custom call to
2159         # 'open-ils.storage.biblio.multiclass.search_fts'
2160         my $method = $self->method_lookup('open-ils.search.biblio.multiclass.query');
2161         my ($search_result) = $method->run({'limit' => 1000000}, "identifier|issn:$issn");
2162         my @recs = map { $_->[0] } @{$search_result->{'ids'}};
2163         return { ids => \@recs, count => $search_result->{'count'} };
2164 }
2165
2166
2167 __PACKAGE__->register_method(
2168     method    => "fetch_mods_by_copy",
2169     api_name  => "open-ils.search.biblio.mods_from_copy",
2170     argc      => 1,
2171     signature => {
2172         desc    => 'Retrieve MODS record given an attached copy ID',
2173         params  => [
2174             { desc => 'Copy ID', type => 'number' }
2175         ],
2176         returns => {
2177             desc => 'MODS record, event on error or uncataloged item'
2178         }
2179     }
2180 );
2181
2182 sub fetch_mods_by_copy {
2183         my( $self, $client, $copyid ) = @_;
2184         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
2185         return $evt if $evt;
2186         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
2187         return $apputils->record_to_mvr($record);
2188 }
2189
2190
2191 # -------------------------------------------------------------------------------------
2192
2193 __PACKAGE__->register_method(
2194     method   => "cn_browse",
2195     api_name => "open-ils.search.callnumber.browse.target",
2196     notes    => "Starts a callnumber browse"
2197 );
2198
2199 __PACKAGE__->register_method(
2200     method   => "cn_browse",
2201     api_name => "open-ils.search.callnumber.browse.page_up",
2202     notes    => "Returns the previous page of callnumbers",
2203 );
2204
2205 __PACKAGE__->register_method(
2206     method   => "cn_browse",
2207     api_name => "open-ils.search.callnumber.browse.page_down",
2208     notes    => "Returns the next page of callnumbers",
2209 );
2210
2211
2212 # RETURNS array of arrays like so: label, owning_lib, record, id
2213 sub cn_browse {
2214         my( $self, $client, @params ) = @_;
2215         my $method;
2216
2217         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
2218                 if( $self->api_name =~ /target/ );
2219         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
2220                 if( $self->api_name =~ /page_up/ );
2221         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
2222                 if( $self->api_name =~ /page_down/ );
2223
2224         return $apputils->simplereq( 'open-ils.storage', $method, @params );
2225 }
2226 # -------------------------------------------------------------------------------------
2227
2228 __PACKAGE__->register_method(
2229     method        => "fetch_cn",
2230     api_name      => "open-ils.search.callnumber.retrieve",
2231     authoritative => 1,
2232     notes         => "retrieves a callnumber based on ID",
2233 );
2234
2235 sub fetch_cn {
2236         my( $self, $client, $id ) = @_;
2237         my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
2238         return $evt if $evt;
2239         return $cn;
2240 }
2241
2242 __PACKAGE__->register_method(
2243     method        => "fetch_fleshed_cn",
2244     api_name      => "open-ils.search.callnumber.fleshed.retrieve",
2245     authoritative => 1,
2246     notes         => "retrieves a callnumber based on ID, fleshing prefix, suffix, and label_class",
2247 );
2248
2249 sub fetch_fleshed_cn {
2250         my( $self, $client, $id ) = @_;
2251         my( $cn, $evt ) = $apputils->fetch_callnumber( $id, 1 );
2252         return $evt if $evt;
2253         return $cn;
2254 }
2255
2256
2257 __PACKAGE__->register_method(
2258     method    => "fetch_copy_by_cn",
2259     api_name  => 'open-ils.search.copies_by_call_number.retrieve',
2260     signature => q/
2261                 Returns an array of copy ID's by callnumber ID
2262                 @param cnid The callnumber ID
2263                 @return An array of copy IDs
2264         /
2265 );
2266
2267 sub fetch_copy_by_cn {
2268         my( $self, $conn, $cnid ) = @_;
2269         return $U->cstorereq(
2270                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
2271                 { call_number => $cnid, deleted => 'f' } );
2272 }
2273
2274 __PACKAGE__->register_method(
2275     method    => 'fetch_cn_by_info',
2276     api_name  => 'open-ils.search.call_number.retrieve_by_info',
2277     signature => q/
2278                 @param label The callnumber label
2279                 @param record The record the cn is attached to
2280                 @param org The owning library of the cn
2281                 @return The callnumber object
2282         /
2283 );
2284
2285
2286 sub fetch_cn_by_info {
2287         my( $self, $conn, $label, $record, $org ) = @_;
2288         return $U->cstorereq(
2289                 'open-ils.cstore.direct.asset.call_number.search',
2290                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
2291 }
2292
2293
2294
2295 __PACKAGE__->register_method(
2296     method   => 'bib_extras',
2297     api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all',
2298     ctype => 'lit_form'
2299 );
2300 __PACKAGE__->register_method(
2301     method   => 'bib_extras',
2302     api_name => 'open-ils.search.biblio.item_form_map.retrieve.all',
2303     ctype => 'item_form'
2304 );
2305 __PACKAGE__->register_method(
2306     method   => 'bib_extras',
2307     api_name => 'open-ils.search.biblio.item_type_map.retrieve.all',
2308     ctype => 'item_type',
2309 );
2310 __PACKAGE__->register_method(
2311     method   => 'bib_extras',
2312     api_name => 'open-ils.search.biblio.bib_level_map.retrieve.all',
2313     ctype => 'bib_level'
2314 );
2315 __PACKAGE__->register_method(
2316     method   => 'bib_extras',
2317     api_name => 'open-ils.search.biblio.audience_map.retrieve.all',
2318     ctype => 'audience'
2319 );
2320
2321 sub bib_extras {
2322         my $self = shift;
2323     $logger->warn("deprecation warning: " .$self->api_name);
2324
2325         my $e = new_editor();
2326
2327     my $ctype = $self->{ctype};
2328     my $ccvms = $e->search_config_coded_value_map({ctype => $ctype});
2329
2330     my @objs;
2331     for my $ccvm (@$ccvms) {
2332         my $obj = "Fieldmapper::config::${ctype}_map"->new;
2333         $obj->value($ccvm->value);
2334         $obj->code($ccvm->code);
2335         $obj->description($ccvm->description) if $obj->can('description');
2336         push(@objs, $obj);
2337     }
2338
2339     return \@objs;
2340 }
2341
2342
2343
2344 __PACKAGE__->register_method(
2345     method    => 'fetch_slim_record',
2346     api_name  => 'open-ils.search.biblio.record_entry.slim.retrieve',
2347     signature => {
2348         desc   => "Retrieves one or more biblio.record_entry without the attached marcxml",
2349         params => [
2350             { desc => 'Array of Record IDs', type => 'array' }
2351         ],
2352         return => { 
2353             desc => 'Array of biblio records, event on error'
2354         }
2355     }
2356 );
2357
2358 sub fetch_slim_record {
2359     my( $self, $conn, $ids ) = @_;
2360
2361 #my $editor = OpenILS::Utils::Editor->new;
2362     my $editor = new_editor();
2363         my @res;
2364     for( @$ids ) {
2365         return $editor->event unless
2366             my $r = $editor->retrieve_biblio_record_entry($_);
2367         $r->clear_marc;
2368         push(@res, $r);
2369     }
2370     return \@res;
2371 }
2372
2373 __PACKAGE__->register_method(
2374     method    => 'rec_hold_parts',
2375     api_name  => 'open-ils.search.biblio.record_hold_parts',
2376     signature => q/
2377        Returns a list of {label :foo, id : bar} objects for viable monograph parts for a given record
2378         /
2379 );
2380
2381 sub rec_hold_parts {
2382         my( $self, $conn, $args ) = @_;
2383
2384     my $rec        = $$args{record};
2385     my $mrec       = $$args{metarecord};
2386     my $pickup_lib = $$args{pickup_lib};
2387     my $e = new_editor();
2388
2389     my $query = {
2390         select => {bmp => ['id', 'label']},
2391         from => 'bmp',
2392         where => {
2393             id => {
2394                 in => {
2395                     select => {'acpm' => ['part']},
2396                     from => {acpm => {acp => {join => {acn => {join => 'bre'}}}}},
2397                     where => {
2398                         '+acp' => {'deleted' => 'f'},
2399                         '+bre' => {id => $rec}
2400                     },
2401                     distinct => 1,
2402                 }
2403             }
2404         }
2405     };
2406
2407     if(defined $pickup_lib) {
2408         my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY);
2409         if($hard_boundary) {
2410             my $orgs = $e->json_query({from => ['actor.org_unit_descendants' => $pickup_lib, $hard_boundary]});
2411             $query->{where}->{'+acp'}->{circ_lib} = [ map { $_->{id} } @$orgs ];
2412         }
2413     }
2414
2415     return $e->json_query($query);
2416 }
2417
2418
2419
2420
2421 __PACKAGE__->register_method(
2422     method    => 'rec_to_mr_rec_descriptors',
2423     api_name  => 'open-ils.search.metabib.record_to_descriptors',
2424     signature => q/
2425                 specialized method...
2426                 Given a biblio record id or a metarecord id, 
2427                 this returns a list of metabib.record_descriptor
2428                 objects that live within the same metarecord
2429                 @param args Object of args including:
2430         /
2431 );
2432
2433 sub rec_to_mr_rec_descriptors {
2434         my( $self, $conn, $args ) = @_;
2435
2436     my $rec        = $$args{record};
2437     my $mrec       = $$args{metarecord};
2438     my $item_forms = $$args{item_forms};
2439     my $item_types = $$args{item_types};
2440     my $item_lang  = $$args{item_lang};
2441     my $pickup_lib = $$args{pickup_lib};
2442
2443     my $hard_boundary = $U->ou_ancestor_setting_value($pickup_lib, OILS_SETTING_HOLD_HARD_BOUNDARY) if (defined $pickup_lib);
2444
2445         my $e = new_editor();
2446         my $recs;
2447
2448         if( !$mrec ) {
2449                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
2450                 return $e->event unless @$map;
2451                 $mrec = $$map[0]->metarecord;
2452         }
2453
2454         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
2455         return $e->event unless @$recs;
2456
2457         my @recs = map { $_->source } @$recs;
2458         my $search = { record => \@recs };
2459         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
2460         $search->{item_type} = $item_types if $item_types and @$item_types;
2461         $search->{item_lang} = $item_lang  if $item_lang;
2462
2463         my $desc = $e->search_metabib_record_descriptor($search);
2464
2465     if ($hard_boundary) { # 0 (or "top") is the same as no setting
2466         my $orgs = $e->json_query(
2467             { from => [ 'actor.org_unit_descendants' => $pickup_lib, $hard_boundary ] }
2468         );
2469
2470         my $good_records = $e->json_query(
2471             { distinct => 1,
2472               select   => { 'bre' => ['id'] },
2473               from     => { 'bre' => { 'acn' => { 'join' => { 'acp' } } } },
2474               where    => {
2475                 '+bre' => { id => \@recs },
2476                 '+acp' => {
2477                     circ_lib => [ map { $_->{id} } @$orgs ],
2478                     deleted  => 'f'
2479                 }
2480               }
2481             }
2482         );
2483
2484         my @keep;
2485         for my $d (@$desc) {
2486             if ( grep { $d->record == $_->{id} } @$good_records ) {
2487                 push @keep, $d;
2488             }
2489         }
2490
2491         $desc = \@keep;
2492     }
2493
2494         return { metarecord => $mrec, descriptors => $desc };
2495 }
2496
2497
2498 __PACKAGE__->register_method(
2499     method   => 'fetch_age_protect',
2500     api_name => 'open-ils.search.copy.age_protect.retrieve.all',
2501 );
2502
2503 sub fetch_age_protect {
2504         return new_editor()->retrieve_all_config_rule_age_hold_protect();
2505 }
2506
2507
2508 __PACKAGE__->register_method(
2509     method   => 'copies_by_cn_label',
2510     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
2511 );
2512
2513 __PACKAGE__->register_method(
2514     method   => 'copies_by_cn_label',
2515     api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
2516 );
2517
2518 sub copies_by_cn_label {
2519         my( $self, $conn, $record, $label, $circ_lib ) = @_;
2520         my $e = new_editor();
2521         my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
2522         return [] unless @$cns;
2523
2524         # show all non-deleted copies in the staff client ...
2525         if ($self->api_name =~ /staff$/o) {
2526                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
2527         }
2528
2529         # ... otherwise, grab the copies ...
2530         my $copies = $e->search_asset_copy(
2531                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
2532                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
2533                 ]
2534         );
2535
2536         # ... and test for location and status visibility
2537         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->opac_visible)) ? ($_->id) : () } @$copies ];
2538 }
2539
2540
2541 1;
2542