1 package OpenILS::Application::Search::Authority;
2 use base qw/OpenILS::Application/;
3 use strict; use warnings;
5 use OpenILS::Utils::Fieldmapper;
6 use OpenILS::Application::AppUtils;
8 use MARC::File::XML (BinaryEncoding => 'UTF-8');
12 use OpenILS::Utils::CStoreEditor q/:funcs/;
13 use OpenSRF::Utils::Logger qw/$logger/;
15 use OpenSRF::Utils::JSON;
17 use Time::HiRes qw(time);
18 use OpenSRF::EX qw(:try);
19 use Digest::MD5 qw(md5_hex);
24 sub validate_authority {
28 my $session = OpenSRF::AppSession->create("open-ils.storage");
29 return $session->request( 'open-ils.storage.authority.validate.tag' => @_ )->gather(1);
31 __PACKAGE__->register_method(
32 method => "validate_authority",
33 api_name => "open-ils.search.authority.validate.tag",
35 note => "Validates authority data from existing controlled terms",
38 sub validate_authority_return_records_by_id {
42 my $session = OpenSRF::AppSession->create("open-ils.storage");
43 return $session->request( 'open-ils.storage.authority.validate.tag.id_list' => @_ )->gather(1);
45 __PACKAGE__->register_method(
46 method => "validate_authority_return_records_by_id",
47 api_name => "open-ils.search.authority.validate.tag.id_list",
49 note => "Validates authority data from existing controlled terms",
52 sub search_authority {
56 my $session = OpenSRF::AppSession->create("open-ils.storage");
57 return $session->request( 'open-ils.storage.authority.search.marc.atomic' => @_ )->gather(1);
59 __PACKAGE__->register_method(
60 method => "search_authority",
61 api_name => "open-ils.search.authority.fts",
63 note => "Searches authority data for existing controlled terms and crossrefs",
66 sub search_authority_by_simple_normalize_heading {
70 my $controlset = shift;
72 my $norm_heading_query = {
73 from => [ 'authority.simple_normalize_heading' => $marcxml ]
77 my $norm_heading = $e->json_query($norm_heading_query)->[0]->{'authority.simple_normalize_heading'};
79 unless (defined($norm_heading) && $norm_heading != '') {
80 return OpenILS::Event->new('BAD_PARAMS', note => 'Heading normalized to null or empty string');
84 select => { are => ['id'] },
89 'startwith' => $norm_heading
91 defined($controlset) ? ( control_set => $controlset ) : ()
95 $client->respond($_->{id}) for @{ $e->json_query( $query ) };
96 $client->respond_complete;
98 __PACKAGE__->register_method(
99 method => "search_authority_by_simple_normalize_heading",
100 api_name => "open-ils.search.authority.simple_heading.from_xml",
103 note => "Searches authority data by main entry using marcxml, returning 'are' ids; params are marcxml and optional control-set-id",
106 sub search_authority_batch_by_simple_normalize_heading {
109 my $search_set = [@_];
111 my $m = $self->method_lookup('open-ils.search.authority.simple_heading.from_xml.atomic');
113 for my $s ( @$search_set ) {
114 for my $k ( keys %$s ) {
115 $client->respond( { $k => $m->run( $s->{$k}, $k ) } );
119 $client->respond_complete;
121 __PACKAGE__->register_method(
122 method => "search_authority_batch_by_simple_normalize_heading",
123 api_name => "open-ils.search.authority.simple_heading.from_xml.batch",
126 note => "Searches authority data by main entry using marcxml, in control-set batches, returning 'are' ids; params are hashes of { control-set-id => marcxml }",
130 sub crossref_authority {
135 my $limit = shift || 10;
137 my $session = OpenSRF::AppSession->create("open-ils.storage");
139 # Avoid generating spurious errors for more granular indexes, like author|personal
140 $class =~ s/^(.*?)\|.*?$/$1/;
142 $logger->info("authority xref search for $class=$term, limit=$limit");
143 my $fr = $session->request(
144 "open-ils.storage.authority.$class.see_from.controlled.atomic",$term, $limit)->gather(1);
145 my $al = $session->request(
146 "open-ils.storage.authority.$class.see_also_from.controlled.atomic",$term, $limit)->gather(1);
148 my $data = _auth_flatten( $term, $fr, $al, 1 );
163 last unless ($$x[$i]);
164 if ($string =~ /\W$/o) {
165 $string .= ' '.$$x[$i];
167 $string .= ' -- '.$$x[$i];
170 next if (lc($string) eq lc($term));
172 $hash{$string}++ if (lc($$x[0]) eq lc($term));
174 my $from = [keys %hash]; #[ sort { $hash{$b} <=> $hash{$a} || $a cmp $b } keys %hash ];
176 # $from = [ @$from[0..4] ] if $limit;
182 last unless ($$x[$i]);
183 if ($string =~ /\W$/o) {
184 $string .= ' '.$$x[$i];
186 $string .= ' -- '.$$x[$i];
189 next if (lc($string) eq lc($term));
191 $hash{$string}++ if (lc($$x[0]) eq lc($term));
193 my $also = [keys %hash]; #[ sort { $hash{$b} <=> $hash{$a} || $a cmp $b } keys %hash ];
195 # $also = [ @$also[0..4] ] if $limit;
197 #warn Dumper( { from => $from, also => $also } );
199 return { from => $from, also => $also };
202 __PACKAGE__->register_method(
203 method => "crossref_authority",
204 api_name => "open-ils.search.authority.crossref",
206 note => "Searches authority data for existing controlled terms and crossrefs",
209 __PACKAGE__->register_method(
210 #method => "new_crossref_authority_batch",
211 method => "crossref_authority_batch2",
212 api_name => "open-ils.search.authority.crossref.batch",
215 Takes an array of class,term pair sub-arrays and performs an authority lookup for each
217 PARAMS( [ ["subject", "earth"], ["author","shakespeare"] ] );
219 Returns an object like so:
222 "term" : { "from" : [ ...], "also" : [...] }
223 "term2" : { "from" : [ ...], "also" : [...] }
228 sub new_crossref_authority_batch {
229 my( $self, $client, $reqs ) = @_;
233 my $session = OpenSRF::AppSession->create("open-ils.storage");
235 for my $req (@$reqs) {
237 my $class = $req->[0];
238 my $term = $req->[1];
239 next unless $class and $term;
240 $logger->info("Sending authority request for $class : $term");
241 my $fr = $session->request("open-ils.storage.authority.$class.see_from.controlled.atomic",$term, 10)->gather(1);
242 my $al = $session->request("open-ils.storage.authority.$class.see_also_from.controlled.atomic",$term, 10)->gather(1);
244 $response->{$class} = {} unless exists $response->{$class};
245 $response->{$class}->{$term} = _auth_flatten( $term, $fr, $al, 1 );
249 #warn Dumper( $response );
253 sub crossref_authority_batch {
254 my( $self, $client, $reqs ) = @_;
258 my $session = OpenSRF::AppSession->create("open-ils.storage");
260 for my $req (@$reqs) {
262 my $class = $req->[0];
263 my $term = $req->[1];
264 next unless $class and $term;
265 $logger->info("Sending authority request for $class : $term");
266 my $freq = $session->request("open-ils.storage.authority.$class.see_from.controlled.atomic",$term, 10);
267 my $areq = $session->request("open-ils.storage.authority.$class.see_also_from.controlled.atomic",$term, 10);
269 if( $lastr->[0] ) { #process old data while waiting on new data
270 my $cls = $lastr->[0];
271 my $trm = $lastr->[1];
272 my $fr = $lastr->[2];
273 my $al = $lastr->[3];
274 $response->{$cls} = {} unless exists $response->{$cls};
275 $response->{$cls}->{$trm} = _auth_flatten( $trm, $fr, $al, 1 );
278 $lastr->[0] = $class;
280 $lastr->[2] = $freq->gather(1);
281 $lastr->[3] = $areq->gather(1);
284 if( $lastr->[0] ) { #process old data while waiting on new data
285 my $cls = $lastr->[0];
286 my $trm = $lastr->[1];
287 my $fr = $lastr->[2];
288 my $al = $lastr->[3];
289 $response->{$cls} = {} unless exists $response->{$cls};
290 $response->{$cls}->{$trm} = _auth_flatten( $trm, $fr, $al, 1);
299 sub crossref_authority_batch2 {
300 my( $self, $client, $reqs ) = @_;
304 my $session = OpenSRF::AppSession->create("open-ils.storage");
306 $cache = OpenSRF::Utils::Cache->new('global') unless $cache;
308 for my $req (@$reqs) {
310 my $class = $req->[0];
311 my $term = $req->[1];
312 next unless $class and $term;
316 my $cdata = $cache->get_cache("oils_authority_${class}_$t");
319 $logger->debug("returning authority response from cache..");
320 $response->{$class} = {} unless exists $response->{$class};
321 $response->{$class}->{$term} = $cdata;
325 $logger->debug("authority data not found in cache.. fetching from storage");
327 $logger->info("Sending authority request for $class : $term");
328 my $freq = $session->request("open-ils.storage.authority.$class.see_from.controlled.atomic",$term, 10);
329 my $areq = $session->request("open-ils.storage.authority.$class.see_also_from.controlled.atomic",$term, 10);
330 my $fr = $freq->gather(1);
331 my $al = $areq->gather(1);
332 $response->{$class} = {} unless exists $response->{$class};
333 my $auth = _auth_flatten( $term, $fr, $al, 1 );
335 my $timeout = 7200; #two hours
336 $timeout = 300 if @{$auth->{from}} or @{$auth->{also}}; # 5 minutes
337 $response->{$class}->{$term} = $auth;
338 $logger->debug("adding authority lookup to cache with timeout $timeout");
339 $cache->put_cache("oils_authority_${class}_$t", $auth, $timeout);
344 __PACKAGE__->register_method(
345 method => "authority_main_entry",
346 api_name => "open-ils.search.authority.main_entry",
350 Returns the main entry details for one or more authority
351 records plus a few other details.
354 {desc => 'Authority IDs', type => 'number or array'}
358 Stream of authority metadata objects.
359 { authority: are_object,
360 heading: heading_text,
361 thesaurus: short_code,
362 thesaurus_code: code,
363 control_set: control_set_object,
364 linked_bib_count: number
372 sub authority_main_entry {
373 my ($self, $client, $auth_ids) = @_;
375 $auth_ids = [$auth_ids] unless ref $auth_ids;
377 my $e = new_editor();
379 for my $auth_id (@$auth_ids) {
381 my $rec = $e->retrieve_authority_record_entry([
384 flesh_fields => {are => [qw/control_set creator/]}
386 ]) or return $e->event;
390 control_set => $rec->control_set
393 $response->{linked_bib_count} = $e->json_query({
395 {column => 'bib', transform => 'count', aggregate => 1}
398 where => {authority => $auth_id}
401 # Extract the heading and thesaurus.
402 # In theory this data has already been extracted in the DB, but
403 # using authority.simple_heading results in data that varies
404 # quite a bit from the previous authority manage interface. I
405 # took the MARC parsing approach because it matches the logic
406 # (and results) of the previous UI.
408 my $marc = MARC::Record->new_from_xml($rec->marc);
409 my $heading_field = $marc->field('1..');
410 $response->{heading} = $heading_field->as_string if $heading_field;
412 my $field_008 = $marc->field('008');
415 # Extract the 1-char thesaurus code from the 008.
416 my $thes = substr($field_008->data, 11, 1);
419 $response->{thesaurus} = $thes;
421 if ($thes ne 'z') { # 'z' ('Other') maps to many entries
422 my $thesaurus = $e->search_authority_thesaurus(
423 {short_code => $thes})->[0];
425 $response->{thesaurus_code} = $thesaurus->code if $thesaurus;
431 $client->respond($response);