]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Search/Biblio.pm
Adding support for field-specific searches. Syntax for use in OpenSearch is {class...
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Search / Biblio.pm
1 package OpenILS::Application::Search::Biblio;
2 use base qw/OpenSRF::Application/;
3 use strict; use warnings;
4
5
6 use 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
13 use OpenSRF::Utils::Logger qw/:logger/;
14
15
16 use JSON;
17
18 use Time::HiRes qw(time);
19 use OpenSRF::EX qw(:try);
20 use Digest::MD5 qw(md5_hex);
21
22 use XML::LibXML;
23 use XML::LibXSLT;
24
25 use Data::Dumper;
26 $Data::Dumper::Indent = 0;
27
28 use OpenILS::Const qw/:const/;
29
30 use OpenILS::Application::AppUtils;
31 my $apputils = "OpenILS::Application::AppUtils";
32 my $U = $apputils;
33
34 my $pfx = "open-ils.search_";
35
36 my $cache;
37 my $cache_timeout;
38
39 sub initialize {
40         $cache = OpenSRF::Utils::Cache->new('global');
41         my $sclient = OpenSRF::Utils::SettingsClient->new();
42         $cache_timeout = $sclient->config_value(
43                         "apps", "open-ils.search", "app_settings", "cache_timeout" ) || 300;
44         $logger->info("Search cache timeout is $cache_timeout");
45 }
46
47
48
49 # ---------------------------------------------------------------------------
50 # takes a list of record id's and turns the docs into friendly 
51 # mods structures. Creates one MODS structure for each doc id.
52 # ---------------------------------------------------------------------------
53 sub _records_to_mods {
54         my @ids = @_;
55         
56         my @results;
57         my @marcxml_objs;
58
59         my $session = OpenSRF::AppSession->create("open-ils.cstore");
60         my $request = $session->request(
61                         "open-ils.cstore.direct.biblio.record_entry.search", { id => \@ids } );
62
63         while( my $resp = $request->recv ) {
64                 my $content = $resp->content;
65                 next if $content->id == OILS_PRECAT_RECORD;
66                 my $u = OpenILS::Utils::ModsParser->new();
67                 $u->start_mods_batch( $content->marc );
68                 my $mods = $u->finish_mods_batch();
69                 $mods->doc_id($content->id());
70                 $mods->tcn($content->tcn_value);
71                 push @results, $mods;
72         }
73
74         $session->disconnect();
75         return \@results;
76 }
77
78 __PACKAGE__->register_method(
79         method  => "record_id_to_mods",
80         api_name        => "open-ils.search.biblio.record.mods.retrieve",
81         argc            => 1, 
82         note            => "Provide ID, we provide the mods"
83 );
84
85 # converts a record into a mods object with copy counts attached
86 sub record_id_to_mods {
87
88         my( $self, $client, $org_id, $id ) = @_;
89
90         my $mods_list = _records_to_mods( $id );
91         my $mods_obj = $mods_list->[0];
92         my $cmethod = $self->method_lookup(
93                         "open-ils.search.biblio.record.copy_count");
94         my ($count) = $cmethod->run($org_id, $id);
95         $mods_obj->copy_count($count);
96
97         return $mods_obj;
98 }
99
100
101
102 __PACKAGE__->register_method(
103         method  => "record_id_to_mods_slim",
104         api_name        => "open-ils.search.biblio.record.mods_slim.retrieve",
105         argc            => 1, 
106         note            => "Provide ID, we provide the mods"
107 );
108
109 # converts a record into a mods object with NO copy counts attached
110 sub record_id_to_mods_slim {
111         my( $self, $client, $id ) = @_;
112         return undef unless defined $id;
113
114         if(ref($id) and ref($id) == 'ARRAY') {
115                 return _records_to_mods( @$id );
116         }
117         my $mods_list = _records_to_mods( $id );
118         my $mods_obj = $mods_list->[0];
119         return OpenILS::Event->new('BIBLIO_RECORD_ENTRY_NOT_FOUND') unless $mods_obj;
120         return $mods_obj;
121 }
122
123
124 # Returns the number of copies attached to a record based on org location
125 __PACKAGE__->register_method(
126         method  => "record_id_to_copy_count",
127         api_name        => "open-ils.search.biblio.record.copy_count",
128 );
129
130 __PACKAGE__->register_method(
131         method  => "record_id_to_copy_count",
132         api_name        => "open-ils.search.biblio.record.copy_count.staff",
133 );
134
135 __PACKAGE__->register_method(
136         method  => "record_id_to_copy_count",
137         api_name        => "open-ils.search.biblio.metarecord.copy_count",
138 );
139
140 __PACKAGE__->register_method(
141         method  => "record_id_to_copy_count",
142         api_name        => "open-ils.search.biblio.metarecord.copy_count.staff",
143 );
144 sub record_id_to_copy_count {
145         my( $self, $client, $org_id, $record_id, $format ) = @_;
146
147         return [] unless $record_id;
148         $format = undef if (!$format or $format eq 'all');
149
150         my $method = "open-ils.storage.biblio.record_entry.copy_count.atomic";
151         my $key = "record";
152
153         if($self->api_name =~ /metarecord/) {
154                 $method = "open-ils.storage.metabib.metarecord.copy_count.atomic";
155                 $key = "metarecord";
156         }
157
158         $method =~ s/atomic/staff\.atomic/og if($self->api_name =~ /staff/ );
159
160         my $count = $U->storagereq( $method, 
161                 org_unit => $org_id, $key => $record_id, format => $format );
162
163         return [ sort { $a->{depth} <=> $b->{depth} } @$count ];
164 }
165
166
167
168
169 __PACKAGE__->register_method(
170         method  => "biblio_search_tcn",
171         api_name        => "open-ils.search.biblio.tcn",
172         argc            => 3, 
173         note            => "Retrieve a record by TCN",
174 );
175
176 sub biblio_search_tcn {
177
178         my( $self, $client, $tcn, $include_deleted ) = @_;
179
180         $tcn =~ s/.*?(\w+)\s*$/$1/o;
181
182         my $e = new_editor();
183    my $search = {tcn_value => $tcn};
184    $search->{deleted} = 'f' unless $include_deleted;
185         my $recs = $e->search_biblio_record_entry( $search, {idlist =>1} );
186         
187         return { count => scalar(@$recs), ids => $recs };
188 }
189
190
191 # --------------------------------------------------------------------------------
192
193 __PACKAGE__->register_method(
194         method  => "biblio_barcode_to_copy",
195         api_name        => "open-ils.search.asset.copy.find_by_barcode",);
196 sub biblio_barcode_to_copy { 
197         my( $self, $client, $barcode ) = @_;
198         my( $copy, $evt ) = $U->fetch_copy_by_barcode($barcode);
199         return $evt if $evt;
200         return $copy;
201 }
202
203 __PACKAGE__->register_method(
204         method  => "biblio_id_to_copy",
205         api_name        => "open-ils.search.asset.copy.batch.retrieve",);
206 sub biblio_id_to_copy { 
207         my( $self, $client, $ids ) = @_;
208         $logger->info("Fetching copies @$ids");
209         return $U->cstorereq(
210                 "open-ils.cstore.direct.asset.copy.search.atomic", { id => $ids } );
211 }
212
213
214 __PACKAGE__->register_method(
215         method  => "copy_retrieve", 
216         api_name        => "open-ils.search.asset.copy.retrieve",);
217 sub copy_retrieve {
218         my( $self, $client, $cid ) = @_;
219         my( $copy, $evt ) = $U->fetch_copy($cid);
220         return $evt if $evt;
221         return $copy;
222 }
223
224 __PACKAGE__->register_method(
225         method  => "volume_retrieve", 
226         api_name        => "open-ils.search.asset.call_number.retrieve");
227 sub volume_retrieve {
228         my( $self, $client, $vid ) = @_;
229         my $e = new_editor();
230         my $vol = $e->retrieve_asset_call_number($vid) or return $e->event;
231         return $vol;
232 }
233
234 __PACKAGE__->register_method(
235         method  => "fleshed_copy_retrieve_batch",
236         api_name        => "open-ils.search.asset.copy.fleshed.batch.retrieve");
237
238 sub fleshed_copy_retrieve_batch { 
239         my( $self, $client, $ids ) = @_;
240         $logger->info("Fetching fleshed copies @$ids");
241         return $U->cstorereq(
242                 "open-ils.cstore.direct.asset.copy.search.atomic",
243                 { id => $ids },
244                 { flesh => 1, 
245                   flesh_fields => { acp => [ qw/ circ_lib location status stat_cat_entries / ] }
246                 });
247 }
248
249
250 __PACKAGE__->register_method(
251         method  => "fleshed_copy_retrieve",
252         api_name        => "open-ils.search.asset.copy.fleshed.retrieve",);
253
254 sub fleshed_copy_retrieve { 
255         my( $self, $client, $id ) = @_;
256         my( $c, $e) = $U->fetch_fleshed_copy($id);
257         return $e if $e;
258         return $c;
259 }
260
261
262
263 __PACKAGE__->register_method(
264         method => 'fleshed_by_barcode',
265         api_name        => "open-ils.search.asset.copy.fleshed2.find_by_barcode",);
266 sub fleshed_by_barcode {
267         my( $self, $conn, $barcode ) = @_;
268         my $e = new_editor();
269         my $copyid = $e->search_asset_copy(
270                 {barcode => $barcode, deleted => 'f'}, {idlist=>1})->[0]
271                 or return $e->event;
272         return $self->fleshed_copy_retrieve2($conn, $copyid);
273 }
274
275
276 __PACKAGE__->register_method(
277         method  => "fleshed_copy_retrieve2",
278         api_name        => "open-ils.search.asset.copy.fleshed2.retrieve",);
279
280 sub fleshed_copy_retrieve2 { 
281         my( $self, $client, $id ) = @_;
282         my $e = new_editor();
283         my $copy = $e->retrieve_asset_copy(
284                 [
285                         $id,
286                         { 
287                                 flesh                           => 2,
288                                 flesh_fields    => { 
289                                         acp => [ qw/ location status stat_cat_entry_copy_maps notes age_protect / ],
290                                         ascecm => [ qw/ stat_cat stat_cat_entry / ],
291                                 }
292                         }
293                 ]
294         ) or return $e->event;
295
296         # For backwards compatibility
297         #$copy->stat_cat_entries($copy->stat_cat_entry_copy_maps);
298
299         if( $copy->status->id == OILS_COPY_STATUS_CHECKED_OUT ) {
300                 $copy->circulations(
301                         $e->search_action_circulation( 
302                                 [       
303                                         { target_copy => $copy->id },
304                                         {
305                                                 order_by => { circ => 'xact_start desc' },
306                                                 limit => 1
307                                         }
308                                 ]
309                         )
310                 );
311         }
312
313         return $copy;
314 }
315
316
317 __PACKAGE__->register_method(
318         method => 'flesh_copy_custom',
319         api_name => 'open-ils.search.asset.copy.fleshed.custom'
320 );
321
322 sub flesh_copy_custom {
323         my( $self, $conn, $copyid, $fields ) = @_;
324         my $e = new_editor();
325         my $copy = $e->retrieve_asset_copy(
326                 [
327                         $copyid,
328                         { 
329                                 flesh                           => 1,
330                                 flesh_fields    => { 
331                                         acp => $fields,
332                                 }
333                         }
334                 ]
335         ) or return $e->event;
336         return $copy;
337 }
338
339
340
341
342
343
344 __PACKAGE__->register_method(
345         method  => "biblio_barcode_to_title",
346         api_name        => "open-ils.search.biblio.find_by_barcode",
347 );
348
349 sub biblio_barcode_to_title {
350         my( $self, $client, $barcode ) = @_;
351
352         my $title = $apputils->simple_scalar_request(
353                 "open-ils.storage",
354                 "open-ils.storage.biblio.record_entry.retrieve_by_barcode", $barcode );
355
356         return { ids => [ $title->id ], count => 1 } if $title;
357         return { count => 0 };
358 }
359
360 __PACKAGE__->register_method(
361     method => 'title_id_by_item_barcode',
362     api_name => 'open-ils.search.bib_id.by_barcode'
363 );
364
365 sub title_id_by_item_barcode {
366     my( $self, $conn, $barcode ) = @_;
367     my $e = new_editor();
368     my $copies = $e->search_asset_copy(
369         [
370             { deleted => 'f', barcode => $barcode },
371             {
372                 flesh => 2,
373                 flesh_fields => {
374                     acp => [ 'call_number' ],
375                     acn => [ 'record' ]
376                 }
377             }
378         ]
379     );
380
381     return $e->event unless @$copies;
382     return $$copies[0]->call_number->record->id;
383 }
384
385
386 __PACKAGE__->register_method(
387         method  => "biblio_copy_to_mods",
388         api_name        => "open-ils.search.biblio.copy.mods.retrieve",
389 );
390
391 # takes a copy object and returns it fleshed mods object
392 sub biblio_copy_to_mods {
393         my( $self, $client, $copy ) = @_;
394
395         my $volume = $U->cstorereq( 
396                 "open-ils.cstore.direct.asset.call_number.retrieve",
397                 $copy->call_number() );
398
399         my $mods = _records_to_mods($volume->record());
400         $mods = shift @$mods;
401         $volume->copies([$copy]);
402         push @{$mods->call_numbers()}, $volume;
403
404         return $mods;
405 }
406
407
408 # ----------------------------------------------------------------------------
409 # These are the main OPAC search methods
410 # ----------------------------------------------------------------------------
411
412 __PACKAGE__->register_method(
413         method          => 'the_quest_for_knowledge',
414         api_name                => 'open-ils.search.biblio.multiclass',
415         signature       => q/
416                 Performs a multi class bilbli or metabib search
417                 @param searchhash A search object layed out like so:
418                         searches : { "$class" : "$value", ...}
419                         org_unit : The org id to focus the search at
420                         depth           : The org depth
421                         limit           : The search limit
422                         offset  : The search offset
423                         format  : The MARC format
424                         sort            : What field to sort the results on [ author | title | pubdate ]
425                         sort_dir        : What direction do we sort? [ asc | desc ]
426                 @return An object of the form 
427                         { "count" : $count, "ids" : [ [ $id, $relevancy, $total ], ...] }
428         /
429 );
430
431 __PACKAGE__->register_method(
432         method          => 'the_quest_for_knowledge',
433         api_name                => 'open-ils.search.biblio.multiclass.staff',
434         signature       => q/@see open-ils.search.biblio.multiclass/);
435 __PACKAGE__->register_method(
436         method          => 'the_quest_for_knowledge',
437         api_name                => 'open-ils.search.metabib.multiclass',
438         signature       => q/@see open-ils.search.biblio.multiclass/);
439 __PACKAGE__->register_method(
440         method          => 'the_quest_for_knowledge',
441         api_name                => 'open-ils.search.metabib.multiclass.staff',
442         signature       => q/@see open-ils.search.biblio.multiclass/);
443
444
445
446 sub the_quest_for_knowledge {
447         my( $self, $conn, $searchhash, $docache ) = @_;
448
449         return { count => 0 } unless $searchhash and
450                 ref $searchhash->{searches} eq 'HASH';
451
452         my $method = 'open-ils.storage.biblio.multiclass.search_fts';
453         my $ismeta = 0;
454         my @recs;
455
456         if($self->api_name =~ /metabib/) {
457                 $ismeta = 1;
458                 $method =~ s/biblio/metabib/o;
459         }
460
461
462         my $offset      = $searchhash->{offset} || 0;
463         my $limit       = $searchhash->{limit} || 10;
464         my $end         = $offset + $limit - 1;
465
466         # do some simple sanity checking
467         if(!$searchhash->{searches} or
468                 ( !grep { /^(?:title|author|subject|series|keyword)/ } keys %{$searchhash->{searches}} ) ) {
469                 return { count => 0 };
470         }
471
472
473         my $maxlimit = 5000;
474         $searchhash->{offset}   = 0;
475         $searchhash->{limit}            = $maxlimit;
476
477         return { count => 0 } if $offset > $maxlimit;
478
479         my @search;
480         push( @search, ($_ => $$searchhash{$_})) for (sort keys %$searchhash);
481         my $s = JSON->perl2JSON(\@search);
482         my $ckey = $pfx . md5_hex($method . $s);
483
484         $logger->info("bib search for: $s");
485
486         $searchhash->{limit} -= $offset;
487
488
489         my $result = ($docache) ? search_cache($ckey, $offset, $limit) : undef;
490
491         if(!$result) {
492
493                 $method .= ".staff" if($self->api_name =~ /staff/);
494                 $method .= ".atomic";
495         
496                 for (keys %$searchhash) { 
497                         delete $$searchhash{$_} 
498                                 unless defined $$searchhash{$_}; 
499                 }
500         
501                 $result = $U->storagereq( $method, %$searchhash );
502
503         } else { 
504                 $docache = 0; 
505         }
506
507         return {count => 0} unless ($result && $$result[0]);
508
509         #for my $r (@$result) { push(@recs, $r) if ($r and $$r[0]); }
510         @recs = @$result;
511
512         my $count = ($ismeta) ? $result->[0]->[3] : $result->[0]->[2];
513
514
515         if( $docache ) {
516
517                 # If we didn't get this data from the cache, put it into the cache
518                 # then return the correct offset of records
519                 $logger->debug("putting search cache $ckey\n");
520                 put_cache($ckey, $count, \@recs);
521
522                 my @t;
523                 for ($offset..$end) {
524                         last unless $recs[$_];
525                         push(@t, $recs[$_]);
526                 }
527                 @recs = @t;
528
529                 #$logger->debug("cache done .. returning $offset..$end : " . JSON->perl2JSON(\@recs));
530         }
531
532         return { ids => \@recs, count => $count };
533 }
534
535
536
537 sub search_cache {
538
539         my $key         = shift;
540         my $offset      = shift;
541         my $limit       = shift;
542         my $start       = $offset;
543         my $end         = $offset + $limit - 1;
544
545         $logger->debug("searching cache for $key : $start..$end\n");
546
547         return undef unless $cache;
548         my $data = $cache->get_cache($key);
549
550         return undef unless $data;
551
552         my $count = $data->[0];
553         $data = $data->[1];
554
555         return undef unless $offset < $count;
556
557
558         my @result;
559         for( my $i = $offset; $i <= $end; $i++ ) {
560                 last unless my $d = $$data[$i];
561                 push( @result, $d );
562         }
563
564         $logger->debug("search_cache found ".scalar(@result)." items for count=$count, start=$start, end=$end");
565
566         return \@result;
567 }
568
569
570 sub put_cache {
571         my( $key, $count, $data ) = @_;
572         return undef unless $cache;
573         $logger->debug("search_cache putting ".
574                 scalar(@$data)." items at key $key with timeout $cache_timeout");
575         $cache->put_cache($key, [ $count, $data ], $cache_timeout);
576 }
577
578
579
580
581
582
583 __PACKAGE__->register_method(
584         method  => "biblio_mrid_to_modsbatch_batch",
585         api_name        => "open-ils.search.biblio.metarecord.mods_slim.batch.retrieve");
586
587 sub biblio_mrid_to_modsbatch_batch {
588         my( $self, $client, $mrids) = @_;
589         warn "Performing mrid_to_modsbatch_batch...";
590         my @mods;
591         my $method = $self->method_lookup("open-ils.search.biblio.metarecord.mods_slim.retrieve");
592         for my $id (@$mrids) {
593                 next unless defined $id;
594                 my ($m) = $method->run($id);
595                 push @mods, $m;
596         }
597         return \@mods;
598 }
599
600
601 __PACKAGE__->register_method(
602         method  => "biblio_mrid_to_modsbatch",
603         api_name        => "open-ils.search.biblio.metarecord.mods_slim.retrieve",
604         notes           => <<"  NOTES");
605         Returns the mvr associated with a given metarecod. If none exists, 
606         it is created.
607         NOTES
608
609 __PACKAGE__->register_method(
610         method  => "biblio_mrid_to_modsbatch",
611         api_name        => "open-ils.search.biblio.metarecord.mods_slim.retrieve.staff",
612         notes           => <<"  NOTES");
613         Returns the mvr associated with a given metarecod. If none exists, 
614         it is created.
615         NOTES
616
617 sub biblio_mrid_to_modsbatch {
618         my( $self, $client, $mrid, $args) = @_;
619
620         warn "Grabbing mvr for $mrid\n";
621
622         my ($mr, $evt) = _grab_metarecord($mrid);
623         return $evt unless $mr;
624
625         my $mvr = $self->biblio_mrid_check_mvr($client, $mr);
626         $mvr = $self->biblio_mrid_make_modsbatch( $client, $mr ) unless $mvr;
627
628         return $mvr unless ref($args);  
629
630         # Here we find the lead record appropriate for the given filters 
631         # and use that for the title and author of the metarecord
632         my $format      = $$args{format};
633         my $org         = $$args{org};
634         my $depth       = $$args{depth};
635
636         return $mvr unless $format or $org or $depth;
637
638         my $method = "open-ils.storage.ordered.metabib.metarecord.records";
639         $method = "$method.staff" if $self->api_name =~ /staff/o; 
640
641         my $rec = $U->storagereq($method, $format, $org, $depth, 1);
642
643         if( my $mods = $U->record_to_mvr($rec) ) {
644
645                 $mvr->title($mods->title);
646                 $mvr->title($mods->author);
647                 $logger->debug("mods_slim updating title and ".
648                         "author in mvr with ".$mods->title." : ".$mods->author);
649         }
650
651         return $mvr;
652 }
653
654 # converts a metarecord to an mvr
655 sub _mr_to_mvr {
656         my $mr = shift;
657         my $perl = JSON->JSON2perl($mr->mods());
658         return Fieldmapper::metabib::virtual_record->new($perl);
659 }
660
661 # checks to see if a metarecord has mods, if so returns true;
662
663 __PACKAGE__->register_method(
664         method  => "biblio_mrid_check_mvr",
665         api_name        => "open-ils.search.biblio.metarecord.mods_slim.check",
666         notes           => <<"  NOTES");
667         Takes a metarecord ID or a metarecord object and returns true
668         if the metarecord already has an mvr associated with it.
669         NOTES
670
671 sub biblio_mrid_check_mvr {
672         my( $self, $client, $mrid ) = @_;
673         my $mr; 
674
675         my $evt;
676         if(ref($mrid)) { $mr = $mrid; } 
677         else { ($mr, $evt) = _grab_metarecord($mrid); }
678         return $evt if $evt;
679
680         warn "Checking mvr for mr " . $mr->id . "\n";
681
682         return _mr_to_mvr($mr) if $mr->mods();
683         return undef;
684 }
685
686 sub _grab_metarecord {
687         my $mrid = shift;
688         #my $e = OpenILS::Utils::Editor->new;
689         my $e = new_editor();
690         my $mr = $e->retrieve_metabib_metarecord($mrid) or return ( undef, $e->event );
691         return ($mr);
692 }
693
694
695 __PACKAGE__->register_method(
696         method  => "biblio_mrid_make_modsbatch",
697         api_name        => "open-ils.search.biblio.metarecord.mods_slim.create",
698         notes           => <<"  NOTES");
699         Takes either a metarecord ID or a metarecord object.
700         Forces the creations of an mvr for the given metarecord.
701         The created mvr is returned.
702         NOTES
703
704 sub biblio_mrid_make_modsbatch {
705         my( $self, $client, $mrid ) = @_;
706
707         #my $e = OpenILS::Utils::Editor->new;
708         my $e = new_editor();
709
710         my $mr;
711         if( ref($mrid) ) {
712                 $mr = $mrid;
713                 $mrid = $mr->id;
714         } else {
715                 $mr = $e->retrieve_metabib_metarecord($mrid) 
716                         or return $e->event;
717         }
718
719         my $masterid = $mr->master_record;
720         $logger->info("creating new mods batch for metarecord=$mrid, master record=$masterid");
721
722         my $ids = $U->storagereq(
723                 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic', $mrid);
724         return undef unless @$ids;
725
726         my $master = $e->retrieve_biblio_record_entry($masterid)
727                 or return $e->event;
728
729         # start the mods batch
730         my $u = OpenILS::Utils::ModsParser->new();
731         $u->start_mods_batch( $master->marc );
732
733         # grab all of the sub-records and shove them into the batch
734         my @ids = grep { $_ ne $masterid } @$ids;
735         #my $subrecs = (@ids) ? $e->batch_retrieve_biblio_record_entry(\@ids) : [];
736
737         my $subrecs = [];
738         if(@$ids) {
739                 for my $i (@$ids) {
740                         my $r = $e->retrieve_biblio_record_entry($i);
741                         push( @$subrecs, $r ) if $r;
742                 }
743         }
744
745         for(@$subrecs) {
746                 $logger->debug("adding record ".$_->id." to mods batch for metarecord=$mrid");
747                 $u->push_mods_batch( $_->marc ) if $_->marc;
748         }
749
750
751         # finish up and send to the client
752         my $mods = $u->finish_mods_batch();
753         $mods->doc_id($mrid);
754         $client->respond_complete($mods);
755
756
757         # now update the mods string in the db
758         my $string = JSON->perl2JSON($mods->decast);
759         $mr->mods($string);
760
761         #$e = OpenILS::Utils::Editor->new(xact => 1);
762         $e = new_editor(xact => 1);
763         $e->update_metabib_metarecord($mr) 
764                 or $logger->error("Error setting mods text on metarecord $mrid : " . Dumper($e->event));
765         $e->finish;
766
767         return undef;
768 }
769
770
771
772
773 # converts a mr id into a list of record ids
774
775 __PACKAGE__->register_method(
776         method  => "biblio_mrid_to_record_ids",
777         api_name        => "open-ils.search.biblio.metarecord_to_records",
778 );
779
780 __PACKAGE__->register_method(
781         method  => "biblio_mrid_to_record_ids",
782         api_name        => "open-ils.search.biblio.metarecord_to_records.staff",
783 );
784
785 sub biblio_mrid_to_record_ids {
786         my( $self, $client, $mrid, $args ) = @_;
787
788         my $format      = $$args{format};
789         my $org         = $$args{org};
790         my $depth       = $$args{depth};
791
792         my $method = "open-ils.storage.ordered.metabib.metarecord.records.atomic";
793         $method =~ s/atomic/staff\.atomic/o if $self->api_name =~ /staff/o; 
794         my $recs = $U->storagereq($method, $mrid, $format, $org, $depth);
795
796         return { count => scalar(@$recs), ids => $recs };
797 }
798
799
800 __PACKAGE__->register_method(
801         method  => "biblio_record_to_marc_html",
802         api_name        => "open-ils.search.biblio.record.html" );
803
804 my $parser              = XML::LibXML->new();
805 my $xslt                        = XML::LibXSLT->new();
806 my $marc_sheet;
807
808 my $settings_client = OpenSRF::Utils::SettingsClient->new();
809 sub biblio_record_to_marc_html {
810         my( $self, $client, $recordid ) = @_;
811
812         if( !$marc_sheet ) {
813                 my $dir = $settings_client->config_value( "dirs", "xsl" );
814                 my $xsl = $settings_client->config_value(
815                         "apps", "open-ils.search", "app_settings", "marc_html_xsl" );
816
817                 $xsl = $parser->parse_file("$dir/$xsl");
818                 $marc_sheet = $xslt->parse_stylesheet( $xsl );
819         }
820
821
822         my $record = $apputils->simple_scalar_request(
823                 "open-ils.cstore", 
824                 "open-ils.cstore.direct.biblio.record_entry.retrieve",
825                 $recordid );
826
827         my $xmldoc = $parser->parse_string($record->marc);
828         my $html = $marc_sheet->transform($xmldoc);
829         $html = $html->toString();
830         return $html;
831
832 }
833
834
835 =head duplicate
836 __PACKAGE__->register_method(
837         method  => "retrieve_all_copy_locations",
838         api_name        => "open-ils.search.config.copy_location.retrieve.all" );
839
840 my $shelving_locations;
841 sub retrieve_all_copy_locations {
842         my( $self, $client ) = @_;
843         if(!$shelving_locations) {
844                 $shelving_locations = $apputils->simple_scalar_request(
845                         "open-ils.cstore", 
846                         "open-ils.cstore.direct.asset.copy_location.search.atomic",
847                         { id => { "!=" => undef } }
848                 );
849         }
850         return $shelving_locations;
851 }
852 =cut
853
854
855
856 __PACKAGE__->register_method(
857         method  => "retrieve_all_copy_statuses",
858         api_name        => "open-ils.search.config.copy_status.retrieve.all" );
859
860 my $copy_statuses;
861 sub retrieve_all_copy_statuses {
862         my( $self, $client ) = @_;
863         return $copy_statuses if $copy_statuses;
864         return $copy_statuses = 
865                 new_editor()->retrieve_all_config_copy_status();
866 }
867
868
869 __PACKAGE__->register_method(
870         method  => "copy_counts_per_org",
871         api_name        => "open-ils.search.biblio.copy_counts.retrieve");
872
873 __PACKAGE__->register_method(
874         method  => "copy_counts_per_org",
875         api_name        => "open-ils.search.biblio.copy_counts.retrieve.staff");
876
877 sub copy_counts_per_org {
878         my( $self, $client, $record_id ) = @_;
879
880         warn "Retreiveing copy copy counts for record $record_id and method " . $self->api_name . "\n";
881
882         my $method = "open-ils.storage.biblio.record_entry.global_copy_count.atomic";
883         if($self->api_name =~ /staff/) { $method =~ s/atomic/staff\.atomic/; }
884
885         my $counts = $apputils->simple_scalar_request(
886                 "open-ils.storage", $method, $record_id );
887
888         $counts = [ sort {$a->[0] <=> $b->[0]} @$counts ];
889         return $counts;
890 }
891
892
893 __PACKAGE__->register_method(
894         method          => "copy_count_summary",
895         api_name        => "open-ils.search.biblio.copy_counts.summary.retrieve",
896         notes           => <<"  NOTES");
897         returns an array of these:
898                 [ org_id, callnumber_label, <status1_count>, <status2_cout>,...]
899                 where statusx is a copy status name.  the statuses are sorted
900                 by id.
901         NOTES
902
903 sub copy_count_summary {
904         my( $self, $client, $rid, $org, $depth ) = @_;
905         $org ||= 1;
906         $depth ||= 0;
907         return $U->storagereq(
908                 'open-ils.storage.biblio.record_entry.status_copy_count.atomic', $rid, $org, $depth );
909 }
910
911
912
913 =head
914 __PACKAGE__->register_method(
915         method          => "multiclass_search",
916         api_name        => "open-ils.search.biblio.multiclass",
917         notes           => <<"  NOTES");
918                 Performs a multiclass search
919                 PARAMS( searchBlob, org_unit, format, limit ) 
920                 where searchBlob is defined like this:
921                         { 
922                                 "title" : { "term" : "water" }, 
923                                 "author" : { "term" : "smith" }, 
924                                 ... 
925                         }
926         NOTES
927
928 __PACKAGE__->register_method(
929         method          => "multiclass_search",
930         api_name        => "open-ils.search.biblio.multiclass.staff",
931         notes           => "see open-ils.search.biblio.multiclass" );
932
933 sub multiclass_search {
934         my( $self, $client, $searchBlob, $orgid, $format, $limit ) = @_;
935
936         $logger->debug("Performing multiclass search with org => $orgid, " .
937                 "format => $format, limit => $limit, and search blob " . Dumper($searchBlob));
938
939         my $meth = 'open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.atomic';
940         if($self->api_name =~ /staff/) { $meth =~ s/metarecord\.atomic/metarecord.staff.atomic/; }
941
942
943         my $records = $apputils->simplereq(
944                 'open-ils.storage', $meth, 
945                  org_unit => $orgid, searches => $searchBlob, format => $format, limit => $limit );
946
947         my $count = 0;
948         my $recs = [];
949
950         if( ref($records) and $records->[0] and 
951                 defined($records->[0]->[3])) { $count = $records->[0]->[3];}
952
953         for my $r (@$records) { push( @$recs, $r ) if ($r and $r->[0]); }
954
955         # records has the form: [ mrid, rank, singleRecord / 0, hitCount ];
956         return { ids => $recs, count => $count };
957 }
958 =cut
959
960
961 =head comment-1
962 __PACKAGE__->register_method(
963         method          => "multiclass_search",
964         api_name                => "open-ils.search.biblio.multiclass",
965         signature       => q/
966                 Performs a multiclass search
967                 @param args A names hash of arguments:
968                         org_unit : The org to focus the search on
969                         depth           : The search depth
970                         format  : Item format
971                         limit           : Return limit
972                         offset  : Search offset
973                         searches : A named hash of searches which has the following format:
974                                 { 
975                                         "title" : { "term" : "water" }, 
976                                         "author" : { "term" : "smith" }, 
977                                         ... 
978                                 }
979                 @return { ids : <array of ids>, count : hitcount }
980         /
981 );
982
983 __PACKAGE__->register_method(
984         method          => "multiclass_search",
985         api_name                => "open-ils.search.biblio.multiclass.staff",
986         notes           => q/@see open-ils.search.biblio.multiclass/ );
987
988 sub multiclass_search {
989         my( $self, $client, $args ) = @_;
990
991         $logger->debug("Performing multiclass search with args:\n" . Dumper($args));
992         my $meth = 'open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.atomic';
993         if($self->api_name =~ /staff/) { $meth =~ s/metarecord\.atomic/metarecord.staff.atomic/; }
994
995         my $records = $apputils->simplereq( 'open-ils.storage', $meth, %$args );
996
997         my $count = 0;
998         my $recs = [];
999
1000         if( ref($records) and $records->[0] and 
1001                 defined($records->[0]->[3])) { $count = $records->[0]->[3];}
1002
1003         for my $r (@$records) { push( @$recs, $r ) if ($r and $r->[0]); }
1004
1005         return { ids => $recs, count => $count };
1006 }
1007
1008 =cut
1009
1010
1011
1012 __PACKAGE__->register_method(
1013         method          => "marc_search",
1014         api_name        => "open-ils.search.biblio.marc.staff");
1015
1016 __PACKAGE__->register_method(
1017         method          => "marc_search",
1018         api_name        => "open-ils.search.biblio.marc",
1019         notes           => <<"  NOTES");
1020                 Example:
1021                 open-ils.storage.biblio.full_rec.multi_search.atomic 
1022                 { "searches": [{"term":"harry","restrict": [{"tag":245,"subfield":"a"}]}], "org_unit": 1,
1023         "limit":5,"sort":"author","item_type":"g"}
1024         NOTES
1025
1026 sub marc_search {
1027         my( $self, $conn, $args, $limit, $offset ) = @_;
1028
1029         my $method = 'open-ils.storage.biblio.full_rec.multi_search';
1030         $method .= ".staff" if $self->api_name =~ /staff/;
1031         $method .= ".atomic";
1032
1033         $limit ||= 10;
1034         $offset ||= 0;
1035
1036         my @search;
1037         push( @search, ($_ => $$args{$_}) ) for (sort keys %$args);
1038         my $ckey = $pfx . md5_hex($method . JSON->perl2JSON(\@search));
1039
1040         my $recs = search_cache($ckey, $offset, $limit);
1041
1042         if(!$recs) {
1043                 $recs = $U->storagereq($method, %$args) || [];
1044                 if( $recs ) {
1045                         put_cache($ckey, scalar(@$recs), $recs);
1046                         $recs = [ @$recs[$offset..($offset + ($limit - 1))] ];
1047                 } else {
1048                         $recs = [];
1049                 }
1050         }
1051
1052         my $count = 0;
1053         $count = $recs->[0]->[2] if $recs->[0] and $recs->[0]->[2];
1054         my @recs = map { $_->[0] } @$recs;
1055
1056         return { ids => \@recs, count => $count };
1057 }
1058
1059
1060 __PACKAGE__->register_method(
1061         method  => "biblio_search_isbn",
1062         api_name        => "open-ils.search.biblio.isbn",
1063 );
1064
1065 sub biblio_search_isbn { 
1066         my( $self, $client, $isbn ) = @_;
1067         $logger->debug("Searching ISBN $isbn");
1068         my $e = new_editor();
1069         my $recs = $U->storagereq(
1070                 'open-ils.storage.id_list.biblio.record_entry.search.isbn.atomic', $isbn );
1071         return { ids => $recs, count => scalar(@$recs) };
1072 }
1073
1074
1075 __PACKAGE__->register_method(
1076         method  => "biblio_search_issn",
1077         api_name        => "open-ils.search.biblio.issn",
1078 );
1079
1080 sub biblio_search_issn { 
1081         my( $self, $client, $issn ) = @_;
1082         $logger->debug("Searching ISSN $issn");
1083         my $e = new_editor();
1084         my $recs = $U->storagereq(
1085                 'open-ils.storage.id_list.biblio.record_entry.search.issn.atomic', $issn );
1086         return { ids => $recs, count => scalar(@$recs) };
1087 }
1088
1089
1090
1091
1092 __PACKAGE__->register_method(
1093         method  => "fetch_mods_by_copy",
1094         api_name        => "open-ils.search.biblio.mods_from_copy",
1095 );
1096
1097 sub fetch_mods_by_copy {
1098         my( $self, $client, $copyid ) = @_;
1099         my ($record, $evt) = $apputils->fetch_record_by_copy( $copyid );
1100         return $evt if $evt;
1101         return OpenILS::Event->new('ITEM_NOT_CATALOGED') unless $record->marc;
1102         return $apputils->record_to_mvr($record);
1103 }
1104
1105
1106
1107 # -------------------------------------------------------------------------------------
1108
1109 __PACKAGE__->register_method(
1110         method  => "cn_browse",
1111         api_name        => "open-ils.search.callnumber.browse.target",
1112         notes           => "Starts a callnumber browse"
1113         );
1114
1115 __PACKAGE__->register_method(
1116         method  => "cn_browse",
1117         api_name        => "open-ils.search.callnumber.browse.page_up",
1118         notes           => "Returns the previous page of callnumbers", 
1119         );
1120
1121 __PACKAGE__->register_method(
1122         method  => "cn_browse",
1123         api_name        => "open-ils.search.callnumber.browse.page_down",
1124         notes           => "Returns the next page of callnumbers", 
1125         );
1126
1127
1128 # RETURNS array of arrays like so: label, owning_lib, record, id
1129 sub cn_browse {
1130         my( $self, $client, @params ) = @_;
1131         my $method;
1132
1133         $method = 'open-ils.storage.asset.call_number.browse.target.atomic' 
1134                 if( $self->api_name =~ /target/ );
1135         $method = 'open-ils.storage.asset.call_number.browse.page_up.atomic'
1136                 if( $self->api_name =~ /page_up/ );
1137         $method = 'open-ils.storage.asset.call_number.browse.page_down.atomic'
1138                 if( $self->api_name =~ /page_down/ );
1139
1140         return $apputils->simplereq( 'open-ils.storage', $method, @params );
1141 }
1142 # -------------------------------------------------------------------------------------
1143
1144 __PACKAGE__->register_method(
1145         method => "fetch_cn",
1146         api_name => "open-ils.search.callnumber.retrieve",
1147         notes           => "retrieves a callnumber based on ID",
1148         );
1149
1150 sub fetch_cn {
1151         my( $self, $client, $id ) = @_;
1152         my( $cn, $evt ) = $apputils->fetch_callnumber( $id );
1153         return $evt if $evt;
1154         return $cn;
1155 }
1156
1157 __PACKAGE__->register_method (
1158         method          => "fetch_copy_by_cn",
1159         api_name                => 'open-ils.search.copies_by_call_number.retrieve',
1160         signature       => q/
1161                 Returns an array of copy id's by callnumber id
1162                 @param cnid The callnumber id
1163                 @return An array of copy ids
1164         /
1165 );
1166
1167 sub fetch_copy_by_cn {
1168         my( $self, $conn, $cnid ) = @_;
1169         return $U->cstorereq(
1170                 'open-ils.cstore.direct.asset.copy.id_list.atomic', 
1171                 { call_number => $cnid, deleted => 'f' } );
1172 }
1173
1174 __PACKAGE__->register_method (
1175         method          => 'fetch_cn_by_info',
1176         api_name                => 'open-ils.search.call_number.retrieve_by_info',
1177         signature       => q/
1178                 @param label The callnumber label
1179                 @param record The record the cn is attached to
1180                 @param org The owning library of the cn
1181                 @return The callnumber object
1182         /
1183 );
1184
1185
1186 sub fetch_cn_by_info {
1187         my( $self, $conn, $label, $record, $org ) = @_;
1188         return $U->cstorereq(
1189                 'open-ils.cstore.direct.asset.call_number.search',
1190                 { label => $label, record => $record, owning_lib => $org, deleted => 'f' });
1191 }
1192
1193
1194                 
1195
1196
1197 __PACKAGE__->register_method (
1198         method => 'bib_extras',
1199         api_name => 'open-ils.search.biblio.lit_form_map.retrieve.all');
1200 __PACKAGE__->register_method (
1201         method => 'bib_extras',
1202         api_name => 'open-ils.search.biblio.item_form_map.retrieve.all');
1203 __PACKAGE__->register_method (
1204         method => 'bib_extras',
1205         api_name => 'open-ils.search.biblio.item_type_map.retrieve.all');
1206 __PACKAGE__->register_method (
1207         method => 'bib_extras',
1208         api_name => 'open-ils.search.biblio.audience_map.retrieve.all');
1209
1210 sub bib_extras {
1211         my $self = shift;
1212
1213         my $e = new_editor();
1214
1215         return $e->retrieve_all_config_lit_form_map()
1216                 if( $self->api_name =~ /lit_form/ );
1217
1218         return $e->retrieve_all_config_item_form_map()
1219                 if( $self->api_name =~ /item_form_map/ );
1220
1221         return $e->retrieve_all_config_item_type_map()
1222                 if( $self->api_name =~ /item_type_map/ );
1223
1224         return $e->retrieve_all_config_audience_map()
1225                 if( $self->api_name =~ /audience_map/ );
1226
1227         return [];
1228 }
1229
1230
1231
1232 __PACKAGE__->register_method(
1233         method  => 'fetch_slim_record',
1234         api_name        => 'open-ils.search.biblio.record_entry.slim.retrieve',
1235         signature=> q/
1236                 Returns a biblio.record_entry without the attached marcxml
1237         /
1238 );
1239
1240 sub fetch_slim_record {
1241         my( $self, $conn, $ids ) = @_;
1242
1243         #my $editor = OpenILS::Utils::Editor->new;
1244         my $editor = new_editor();
1245         my @res;
1246         for( @$ids ) {
1247                 return $editor->event unless
1248                         my $r = $editor->retrieve_biblio_record_entry($_);
1249                 $r->clear_marc;
1250                 push(@res, $r);
1251         }
1252         return \@res;
1253 }
1254
1255
1256
1257 __PACKAGE__->register_method(
1258         method => 'rec_to_mr_rec_descriptors',
1259         api_name        => 'open-ils.search.metabib.record_to_descriptors',
1260         signature       => q/
1261                 specialized method...
1262                 Given a biblio record id or a metarecord id, 
1263                 this returns a list of metabib.record_descriptor
1264                 objects that live within the same metarecord
1265                 @param args Object of args including:
1266         /
1267 );
1268
1269 sub rec_to_mr_rec_descriptors {
1270         my( $self, $conn, $args ) = @_;
1271
1272         my $rec = $$args{record};
1273         my $mrec        = $$args{metarecord};
1274         my $item_forms = $$args{item_forms};
1275         my $item_types  = $$args{item_types};
1276         my $item_lang   = $$args{item_lang};
1277
1278         my $e = new_editor();
1279         my $recs;
1280
1281         if( !$mrec ) {
1282                 my $map = $e->search_metabib_metarecord_source_map({source => $rec});
1283                 return $e->event unless @$map;
1284                 $mrec = $$map[0]->metarecord;
1285         }
1286
1287         $recs = $e->search_metabib_metarecord_source_map({metarecord => $mrec});
1288         return $e->event unless @$recs;
1289
1290         my @recs = map { $_->source } @$recs;
1291         my $search = { record => \@recs };
1292         $search->{item_form} = $item_forms if $item_forms and @$item_forms;
1293         $search->{item_type} = $item_types if $item_types and @$item_types;
1294         $search->{item_lang} = $item_lang if $item_lang;
1295
1296         my $desc = $e->search_metabib_record_descriptor($search);
1297
1298         return { metarecord => $mrec, descriptors => $desc };
1299 }
1300
1301
1302
1303
1304 __PACKAGE__->register_method(
1305         method => 'copies_created_on',  
1306 );
1307
1308
1309 sub copies_created_on {
1310         my( $self, $conn, $auth, $org, $date ) = @_;
1311         my $e = new_editor(authtoken=>$auth);
1312         return $e->event unless $e->checkauth;
1313 }
1314
1315
1316 __PACKAGE__->register_method(
1317         method => 'fetch_age_protect',
1318         api_name => 'open-ils.search.copy.age_protect.retrieve.all',
1319 );
1320
1321 sub fetch_age_protect {
1322         return new_editor()->retrieve_all_config_rule_age_hold_protect();
1323 }
1324
1325
1326 __PACKAGE__->register_method(
1327         method => 'copies_by_cn_label',
1328         api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label',
1329 );
1330
1331 __PACKAGE__->register_method(
1332         method => 'copies_by_cn_label',
1333         api_name => 'open-ils.search.asset.copy.retrieve_by_cn_label.staff',
1334 );
1335
1336 sub copies_by_cn_label {
1337         my( $self, $conn, $record, $label, $circ_lib ) = @_;
1338         my $e = new_editor();
1339         my $cns = $e->search_asset_call_number({record => $record, label => $label, deleted => 'f'}, {idlist=>1});
1340         return [] unless @$cns;
1341
1342         # show all non-deleted copies in the staff client ...
1343         if ($self->api_name =~ /staff$/o) {
1344                 return $e->search_asset_copy({call_number => $cns, circ_lib => $circ_lib, deleted => 'f'}, {idlist=>1});
1345         }
1346
1347         # ... otherwise, grab the copies ...
1348         my $copies = $e->search_asset_copy(
1349                 [ {call_number => $cns, circ_lib => $circ_lib, deleted => 'f', opac_visible => 't'},
1350                   {flesh => 1, flesh_fields => { acp => [ qw/location status/] } }
1351                 ]
1352         );
1353
1354         # ... and test for location and status visibility
1355         return [ map { ($U->is_true($_->location->opac_visible) && $U->is_true($_->status->holdable)) ? ($_->id) : () } @$copies ];
1356 }
1357
1358
1359
1360 1;
1361
1362