]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/Authority.pm
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Cat / Authority.pm
1 package OpenILS::Application::Cat::Authority;
2 use strict; use warnings;
3 use MARC::Record;
4 use MARC::File::XML (BinaryEncoding => 'utf8', RecordFormat => 'USMARC');
5 use base qw/OpenILS::Application/;
6 use OpenILS::Utils::CStoreEditor q/:funcs/;
7 use OpenILS::Application::Cat::AuthCommon;
8 use OpenSRF::Utils::Logger qw($logger);
9 use OpenILS::Application::AppUtils;
10 use OpenILS::Utils::Fieldmapper;
11 use OpenILS::Const qw/:const/;
12 use OpenILS::Event;
13 my $U = 'OpenILS::Application::AppUtils';
14
15 # generate a MARC XML document from a MARC XML string
16 sub marc_xml_to_doc {
17     my $xml = shift;
18     return $U->marc_xml_to_doc($xml);
19 }
20
21 __PACKAGE__->register_method(
22     method  => 'import_authority_record',
23     api_name    => 'open-ils.cat.authority.record.import',
24 );
25
26 sub import_authority_record {
27     my($self, $conn, $auth, $marc_xml, $source) = @_;
28     my $e = new_editor(authtoken=>$auth, xact=>1);
29     return $e->die_event unless $e->checkauth;
30     return $e->die_event unless $e->allowed('CREATE_AUTHORITY_RECORD');
31     my $rec = OpenILS::Application::Cat::AuthCommon->
32         import_authority_record($e, $marc_xml, $source);
33     $e->commit unless $U->event_code($rec);
34     return $rec;
35 }
36
37 __PACKAGE__->register_method(
38     method => 'create_authority_record_from_bib_field',
39     api_name => 'open-ils.cat.authority.record.create_from_bib',
40     signature => {
41         desc => q/Create an authority record entry from a field in a bibliographic record/,
42         params => q/
43             @param field A hash representing the field to control, consisting of: { tag: string, ind1: string, ind2: string, subfields: [ [code, value] ... ] }
44             @param identifier A MARC control number identifier
45             @param authtoken A valid authentication token
46             @returns The new record object 
47  /}
48 );
49
50 __PACKAGE__->register_method(
51     method => 'create_authority_record_from_bib_field',
52     api_name => 'open-ils.cat.authority.record.create_from_bib.readonly',
53     signature => {
54         desc => q/Creates MARCXML for an authority record entry from a field in a bibliographic record/,
55         params => q/
56             @param field A hash representing the field to control, consisting of: { tag: string, ind1: string, ind2: string, subfields: [ [code, value] ... ] }
57             @param identifier A MARC control number identifier
58             @returns The MARCXML for the authority record
59  /}
60 );
61
62 sub create_authority_record_from_bib_field {
63     my($self, $conn, $field, $cni, $auth) = @_;
64
65     # Control number identifier should have been passed in
66     if (!$cni) {
67         $cni = 'UNSET';
68     }
69
70     # Change the first character of the incoming bib field tag to a '1'
71     # for use in our authority record; close enough for now?
72     my $tag = $field->{'tag'};
73     $tag =~ s/^./1/;
74
75     my $ind1 = $field->{ind1} || ' ';
76     my $ind2 = $field->{ind2} || ' ';
77
78     my $control = qq{<datafield tag="$tag" ind1="$ind1" ind2="$ind2">};
79     foreach my $sf (@{$field->{subfields}}) {
80         my $code = $sf->[0];
81         my $val = $U->entityize($sf->[1]);
82         $control .= qq{<subfield code="$code">$val</subfield>};
83     }
84     $control .= '</datafield>';
85
86     # ARN, or "authority record number", used to need to be unique across the database.
87     # Of course, we have no idea what's in the database, and if the
88     # cat.maintain_control_numbers flag is set to "TRUE" then the 001 will
89     # be reset to the record ID anyway.
90     my $arn = 'AUTOGEN-' . time();
91
92     # Placeholder MARCXML; 
93     #   001/003 can be be properly filled in via database triggers
94     #   005 will be filled in automatically at creation time
95     #   008 needs to be set by a cataloguer (could be some OU settings, I suppose)
96     #   040 should come from OU settings / OU shortname
97     #   
98     my $marc_xml = <<MARCXML;
99 <record xmlns:marc="http://www.loc.gov/MARC21/slim" xmlns="http://www.loc.gov/MARC21/slim"><leader>     nz  a22     o  4500</leader>
100 <controlfield tag="001">$arn</controlfield>
101 <controlfield tag="008">      ||||||||||||||||||||||||||||||||||</controlfield>
102 <datafield tag="040" ind1=" " ind2=" "><subfield code="a">$cni</subfield><subfield code="c">$cni</subfield></datafield>
103 $control
104 </record>
105 MARCXML
106
107     if ($self->api_name =~ m/readonly$/) {
108         return $marc_xml;
109     } else {
110         my $e = new_editor(authtoken=>$auth, xact=>1);
111         return $e->die_event unless $e->checkauth;
112         return $e->die_event unless $e->allowed('CREATE_AUTHORITY_RECORD');
113         my $rec = OpenILS::Application::Cat::AuthCommon->import_authority_record($e, $marc_xml);
114         $e->commit unless $U->event_code($rec);
115         return $rec;
116     }
117 }
118
119 __PACKAGE__->register_method(
120     method  => 'overlay_authority_record',
121     api_name    => 'open-ils.cat.authority.record.overlay',
122 );
123
124 sub overlay_authority_record {
125     my($self, $conn, $auth, $rec_id, $marc_xml, $source) = @_;
126     my $e = new_editor(authtoken=>$auth, xact=>1);
127     return $e->die_event unless $e->checkauth;
128     return $e->die_event unless $e->allowed('UPDATE_AUTHORITY_RECORD');
129     my $rec = OpenILS::Application::Cat::AuthCommon->
130         overlay_authority_record($e, $rec_id, $marc_xml, $source);
131     $e->commit unless $U->event_code($rec);
132     return $rec;
133 }
134
135 __PACKAGE__->register_method(
136     method  => 'retrieve_authority_record',
137     api_name    => 'open-ils.cat.authority.record.retrieve',
138     signature => {
139         desc => q/Retrieve an authority record entry/,
140         params => [
141             {desc => q/hash of options.  Options include "clear_marc" which clears
142                 the MARC xml from the record before it is returned/}
143         ]
144     }
145 );
146 sub retrieve_authority_record {
147     my($self, $conn, $auth, $rec_id, $options) = @_;
148     my $e = new_editor(authtoken=>$auth);
149     return $e->die_event unless $e->checkauth;
150     my $rec = $e->retrieve_authority_record_entry($rec_id) or return $e->event;
151     $rec->clear_marc if $$options{clear_marc};
152     return $rec;
153 }
154
155 __PACKAGE__->register_method(
156     method  => 'batch_retrieve_authority_record',
157     api_name    => 'open-ils.cat.authority.record.batch.retrieve',
158     stream => 1,
159     signature => {
160         desc => q/Retrieve a set of authority record entry objects/,
161         params => [
162             {desc => q/hash of options.  Options include "clear_marc" which clears
163                 the MARC xml from the record before it is returned/}
164         ]
165     }
166 );
167 sub batch_retrieve_authority_record {
168     my($self, $conn, $auth, $rec_id_list, $options) = @_;
169     my $e = new_editor(authtoken=>$auth);
170     return $e->die_event unless $e->checkauth;
171     for my $rec_id (@$rec_id_list) {
172         my $rec = $e->retrieve_authority_record_entry($rec_id) or return $e->event;
173         $rec->clear_marc if $$options{clear_marc};
174         $conn->respond($rec);
175     }
176     return undef;
177 }
178
179 __PACKAGE__->register_method(
180     method    => 'count_linked_bibs',
181     api_name  => 'open-ils.cat.authority.records.count_linked_bibs',
182     signature => q/
183         Counts the number of bib records linked to each authority record in the input list
184         @param records Array of authority records to return counts
185         @return A list of hashes containing the authority record ID ("id") and linked bib count ("bibs")
186     /
187 );
188
189 sub count_linked_bibs {
190     my( $self, $conn, $records ) = @_;
191
192     my $editor = new_editor();
193
194     my $link_count = [];
195     my @clean_records;
196     for my $auth ( @$records ) {
197         # Protection against SQL injection? Might be overkill.
198         my $intauth = int($auth);
199         if ($intauth) {
200             push(@clean_records, $intauth);
201         }
202     }
203     return $link_count if !@clean_records;
204     
205     $link_count = $editor->json_query({
206         "select" => {
207             "abl" => [
208                 {
209                     "column" => "authority"
210                 },
211                 {
212                     "alias" => "bibs",
213                     "transform" => "count",
214                     "column" => "bib",
215                     "aggregate" => 1
216                 }
217             ]
218         },
219         "from" => "abl",
220         "where" => { "authority" => \@clean_records }
221     });
222
223     return $link_count;
224 }
225
226 __PACKAGE__->register_method(
227     "method" => "retrieve_acs",
228     "api_name" => "open-ils.cat.authority.control_set.retrieve",
229     "api_level" => 1,
230     "stream" => 1,
231     "argc" => 2,
232     "signature" => {
233         "desc" => q/Retrieve authority.control_set objects with fleshed
234         thesauri and authority fields/,
235         "params" => [
236             {"name" => "limit",  "desc" => "limit (optional; default 15)", "type" => "number"},
237             {"name" => "offset",  "desc" => "offset doptional; default 0)", "type" => "number"},
238             {"name" => "focus",  "desc" => "optionally make sure the acs object with ID matching this value comes at the top of the result set (only works with offset 0)", "type" => "number"}
239         ]
240     }
241 );
242
243 # XXX I don't think this really needs to be protected by perms, or does it?
244 sub retrieve_acs {
245     my $self = shift;
246     my $client = shift;
247
248     my ($limit, $offset, $focus) = map int, @_;
249
250     $limit ||= 15;
251     $offset ||= 0;
252     $focus ||= undef;
253
254     my $e = new_editor;
255     my $order_by = [
256         {"class" => "acs", "field" => "name"}
257     ];
258
259     # Here is the magic that let's us say that a given acsaf
260     # will be our first result.
261     unshift @$order_by, {
262         "class" => "acs", "field" => "id",
263         "transform" => "numeric_eq", "params" => [$focus],
264         "direction" => "desc"
265     } if $focus;
266
267     my $sets = $e->search_authority_control_set([
268         {"id" => {"!=" => undef}}, {
269             "flesh" => 1,
270             "flesh_fields" => {"acs" => [qw/thesauri authority_fields/]},
271             "order_by" => $order_by,
272             "limit" => $limit,
273             "offset" => $offset
274         }
275     ]) or return $e->die_event;
276
277     $e->disconnect;
278
279     $client->respond($_) foreach @$sets;
280     return undef;
281 }
282
283 __PACKAGE__->register_method(
284     "method" => "retrieve_acsaf",
285     "api_name" => "open-ils.cat.authority.control_set_authority_field.retrieve",
286     "api_level" => 1,
287     "stream" => 1,
288     "argc" => 2,
289     "signature" => {
290         "desc" => q/Retrieve authority.control_set_authority_field objects with
291         fleshed bib_fields and axes/,
292         "params" => [
293             {"name" => "limit",  "desc" => "limit (optional; default 15)", "type" => "number"},
294             {"name" => "offset",  "desc" => "offset (optional; default 0)", "type" => "number"},
295             {"name" => "control_set",  "desc" => "optionally constrain by value of acsaf.control_set field", "type" => "number"},
296             {"name" => "focus", "desc" => "optionally make sure the acsaf object with ID matching this value comes at the top of the result set (only works with offset 0)"}
297         ]
298     }
299 );
300
301 sub retrieve_acsaf {
302     my $self = shift;
303     my $client = shift;
304
305     my ($limit, $offset, $control_set, $focus) = map int, @_;
306
307     $limit ||= 15;
308     $offset ||= 0;
309     $control_set ||= undef;
310     $focus ||= undef;
311
312     my $e = new_editor;
313     my $where = {
314         "control_set" => ($control_set ? $control_set : {"!=" => undef})
315     };
316     my $order_by = [
317         {"class" => "acsaf", "field" => "main_entry", "direction" => "desc"},
318         {"class" => "acsaf", "field" => "id"}
319     ];
320
321     unshift @$order_by, {
322         "class" => "acsaf", "field" => "id",
323         "transform" => "numeric_eq", "params" => [$focus],
324         "direction" => "desc"
325     } if $focus;
326
327     my $fields = $e->search_authority_control_set_authority_field([
328         $where, {
329             "flesh" => 2,
330             "flesh_fields" => {
331                 "acsaf" => ["bib_fields", "axis_maps"],
332                 "abaafm" => ["axis"]
333             },
334             "order_by" => $order_by,
335             "limit" => $limit,
336             "offset" => $offset
337         }
338     ]) or return $e->die_event;
339
340     $e->disconnect;
341
342     $client->respond($_) foreach @$fields;
343     return undef;
344 }
345
346 __PACKAGE__->register_method(
347     method => "bib_field_overlay_authority_field",
348     api_name => "open-ils.cat.authority.bib_field.overlay_authority",
349     api_level => 1,
350     stream => 1,
351     argc => 2,
352     signature => {
353         desc => q/Given a bib field hash and an authority field hash,
354             merge the authority data for controlled fields into the 
355             bib field./,
356         params => [
357             {name => 'Bib Field', 
358                 desc => '{tag:., ind1:., ind2:.,subfields:[[code, value],...]}'},
359             {name => 'Authority Field', 
360                 desc => '{tag:., ind1:., ind2:.,subfields:[[code, value],...]}'},
361             {name => 'Control Set ID',
362                 desc => q/Optional control set limiter.  If no control set
363                     is provided, the first matching authority field
364                     definition will be used./}
365         ],
366         return => q/The modified bib field/
367     }
368 );
369
370 # Returns the first found field.
371 sub get_auth_field_by_tag {
372     my ($atag, $cset_id) = @_;
373
374     my $e = new_editor();
375
376     my $where = {tag => $atag};
377
378     $where->{control_set} = $cset_id if $cset_id;
379
380     return $e->search_authority_control_set_authority_field($where)->[0];
381 }
382
383 sub bib_field_overlay_authority_field {
384     my ($self, $client, $bib_field, $auth_field, $cset_id) = @_;
385
386     return $bib_field unless $bib_field && $auth_field;
387
388     my $btag = $bib_field->{'tag'};
389     my $atag = $auth_field->{'tag'};
390
391     # Find the controlled subfields.  Here we assume the authority
392     # field provided should be used as the source of which subfields
393     # are controlled.  If passed a set of bib and auth data that are
394     # not consistent with the control set, it may produce unexpected
395     # results.
396     my $sf_list = '';
397     my $acsaf = get_auth_field_by_tag($atag, $cset_id);
398
399     if ($acsaf) {
400         $sf_list = $acsaf->sf_list;
401
402     } else {
403
404         # Handle 4XX and 5XX
405         (my $alt_atag = $atag) =~ s/^./1/;
406         $acsaf = get_auth_field_by_tag($alt_atag, $cset_id) if $alt_atag ne $atag;
407
408         $sf_list = $acsaf->sf_list if $acsaf;
409     }
410
411     my $subfields = [];
412     my $auth_sf_zero;
413
414     # Add the controlled authority subfields
415     for my $sf (@{$auth_field->{subfields}}) {
416         my $c = $sf->[0]; # subfield code
417         my $v = $sf->[1]; # subfield value
418
419         if ($c eq '0') {
420             $auth_sf_zero = $v;
421
422         } elsif (index($sf_list, $c) > -1) {
423             push(@$subfields, [$c, $v]);
424         }
425     }
426
427     # Add the uncontrolled bib subfields
428     for my $sf (@{$bib_field->{subfields}}) {
429         my $c = $sf->[0]; # subfield code
430         my $v = $sf->[1]; # subfield value
431
432         # Discard the bib '0' since the link is no longer valid, 
433         # given we're replacing the contents of the field.
434         if (index($sf_list, $c) < 0 && $c ne '0') {
435             push(@$subfields, [$c, $v]);
436         }
437     }
438
439     # The data on this authority field may link to yet 
440     # another authority record.  Track that in our bib field
441     # as the last subfield;
442     push(@$subfields, ['0', $auth_sf_zero]) if $auth_sf_zero;
443
444     my $new_bib_field = {
445         tag => $bib_field->{tag},
446         ind1 => $auth_field->{'ind1'},
447         ind2 => $auth_field->{'ind2'},
448         subfields => $subfields
449     };
450
451     $new_bib_field->{ind1} = $auth_field->{'ind2'} 
452         if $atag eq '130' && $btag eq '130';
453
454     return $new_bib_field;
455 }
456
457 __PACKAGE__->register_method(
458     method    => "validate_bib_fields",
459     api_name  => "open-ils.cat.authority.validate.bib_field",
460     stream => 1,
461     signature => {
462         desc => q/Returns a stream of bib field objects with a 'valid'
463         attribute added, set to 1 or 0, indicating whether the field
464         has a matching authority entry.  If no control set ID is provided
465         all configured control sets will be tested.  Testing will stop
466         with the first positive validation./,
467         params => [
468             {type => 'object', name => 'Bib Fields',
469                 description => q/
470                     List of objects like this 
471                     {
472                         tag: tag, 
473                         ind1: ind1, 
474                         ind2: ind2, 
475                         subfields: [[code, value], ...]
476                     }
477
478                     For example:
479 srfsh# request open-ils.cat open-ils.cat.authority.validate.bib_field
480   [{"tag":"600","ind1":"", "ind2":"", "subfields":[["a","shakespeare william"], ...]}]
481                 /
482             },
483             {type => 'number', name => 'Optional Control Set ID'},
484         ]
485     }
486 );
487
488 # for stub records sent to 
489 # open-ils.cat.authority.simple_heading
490 my $auth_leader = '00000czm a2200205Ka 4500';
491
492 sub validate_bib_fields {
493     my ($self, $client, $bib_fields, $control_set) = @_;
494
495     $bib_fields = [$bib_fields] unless ref $bib_fields eq 'ARRAY';
496
497     my $e = new_editor();
498
499     for my $bib_field (@$bib_fields) {
500
501         $bib_field->{valid} = 0;
502
503         my $where = {'+acsbf' => {tag => $bib_field->{tag}}};
504         $where->{'+acsaf'} = {control_set => $control_set} if $control_set;
505
506         my $auth_field_list = $e->json_query({
507             select => {
508                 acsbf => ['authority_field'],
509                 acsaf => ['id', 'tag', 'sf_list', 'control_set']
510             },
511             from => {acsbf => {acsaf => {}}},
512             where => $where,
513             order_by => [
514                 {class => 'acsaf', field => 'main_entry', direction => 'desc'},
515                 {class => 'acsaf', field => 'tag'}
516             ]
517         });
518
519         my @seen_subfields;
520         for my $auth_field (@$auth_field_list) {
521
522             my $sf_list = $auth_field->{sf_list};
523
524             # Some auth fields have the same sf_list values.  Track the
525             # ones we've already tested.
526             next if grep {$_ eq $sf_list} @seen_subfields;
527
528             push(@seen_subfields, $sf_list);
529
530             my @sf_values;
531             for my $subfield (@{$bib_field->{subfields}}) {
532                 my $code = $subfield->[0];
533                 my $value = $subfield->[1];
534
535                 next unless defined $value && $value ne '';
536
537                 # is this a controlled subfield?
538                 next unless index($sf_list, $code) > -1;
539
540                 push(@sf_values, $code, $value);
541             }
542
543             next unless @sf_values;
544
545             my $record = MARC::Record->new;
546             $record->leader($auth_leader);
547
548             my $field = MARC::Field->new($auth_field->{tag},
549                 $bib_field->{ind1}, $bib_field->{ind2}, @sf_values);
550
551             $record->append_fields($field);
552
553             my $match = $U->simplereq(
554                 'open-ils.search', 
555                 'open-ils.search.authority.simple_heading.from_xml',
556                 $record->as_xml_record, $control_set);
557
558             if ($match) {
559                 $bib_field->{valid} = 1;
560                 $bib_field->{authority_record} = $match;
561                 $bib_field->{authority_field} = $auth_field->{id};
562                 $bib_field->{control_set} = $auth_field->{control_set};
563                 last;
564             }
565         }
566
567         # Present our findings.
568         $client->respond($bib_field);
569     }
570
571     return undef;
572 }
573
574
575 __PACKAGE__->register_method(
576     method    => "bib_field_authority_linking_browse",
577     api_name  => "open-ils.cat.authority.bib_field.linking_browse",
578     stream => 1,
579     signature => {
580         desc => q/Returns a stream of authority record blobs including
581             information on its main heading and its see froms and see 
582             alsos, based on an axis-based browse search.  This was
583             initially created to move some MARC editor authority linking 
584             logic to the server.  The browse axis is derived from the
585             bib field data provided.
586         ...
587         /,
588         params => [
589             {type => 'object', name => 'MARC Field hash {tag:.,ind1:.,ind2:,subfields:[[code,value],.]}'},
590             {type => 'number', name => 'Page size / limit'},
591             {type => 'number', name => 'Page offset'},
592             {type => 'string', name => 'Optional thesauri, comma separated'}
593         ]
594     }
595 );
596
597 sub get_heading_string {
598     my $field = shift;
599
600     my $heading = '';
601     for my $subfield ($field->subfields) {
602         $heading .= ' --' if index('xyz', $subfield->[0]) > -1;
603         $heading .= ' ' if $heading;
604         $heading .= $subfield->[1];
605     }
606
607     return $heading;
608 }
609
610 # Turns a MARC::Field into a hash and adds the field's heading string.
611 sub hashify_field {
612     my $field = shift;
613     return {
614         heading => get_heading_string($field),
615         tag => $field->tag,
616         ind1 => $field->indicator(1),
617         ind2 => $field->indicator(2),
618         subfields => [$field->subfields]
619     };
620 }
621
622 sub bib_field_authority_linking_browse {
623     my ($self, $client, $bib_field, $limit, $offset, $thesauri) = @_;
624
625     $offset ||= 0;
626     $limit ||= 5;
627     $thesauri ||= '';
628     my $e = new_editor();
629
630     return [] unless $bib_field;
631
632     my $term = join(' ', map {$_->[1]} @{$bib_field->{subfields}});
633
634     return [] unless $term;
635
636     my $axis = $e->json_query({
637         select => {abaafm => ['axis']},
638         from => {acsbf => {acsaf => {join => 'abaafm'}}},
639         where => {'+acsbf' => {tag => $bib_field->{tag}}},
640         order_by => [
641             {class => 'acsaf', field => 'main_entry', direction => 'desc'},
642             {class => 'acsaf', field => 'tag'},
643
644             # This lets us favor the 'subject' axis to the 'topic' axis.
645             # Topic is a subset of subject.  It's not clear if a field
646             # can link only to the 'topic' axes.  In stock EG, the one
647             # 'topic' field also links to 'subject'.
648             {class => 'abaafm', field => 'axis'}
649         ]
650     })->[0];
651
652     return [] unless $axis && ($axis = $axis->{axis});
653
654     # See https://bugs.launchpad.net/evergreen/+bug/1403098
655     my $are_ids = $U->simplereq(
656         'open-ils.supercat',
657         'open-ils.supercat.authority.browse_center.by_axis.refs',
658         $axis, $term, $offset, $limit, $thesauri);
659
660     for my $are_id (@$are_ids) {
661
662         my $are = $e->retrieve_authority_record_entry($are_id);
663         my $rec = MARC::Record->new_from_xml($are->marc, 'UTF-8');
664
665         my $main_field = $rec->field('1..');
666         my $auth_org_field = $rec->field('003');
667         my $auth_org = $auth_org_field ? $auth_org_field->data : undef;
668
669         my $resp = {
670             authority_id => $are_id,
671             main_heading => hashify_field($main_field),
672             auth_org => $auth_org,
673             see_alsos => [],
674             see_froms => []
675         };
676
677         for my $also_field ($rec->field('5..')) {
678             push(@{$resp->{see_alsos}}, hashify_field($also_field));
679         }
680
681         for my $from_field ($rec->field('4..')) {
682             push(@{$resp->{see_froms}}, hashify_field($from_field));
683         }
684
685         $client->respond($resp);
686     }
687
688     return undef;
689 }
690
691 1;