1 package OpenILS::Application::Cat::Authority;
2 use strict; use warnings;
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/;
13 my $U = 'OpenILS::Application::AppUtils';
15 # generate a MARC XML document from a MARC XML string
18 return $U->marc_xml_to_doc($xml);
21 __PACKAGE__->register_method(
22 method => 'import_authority_record',
23 api_name => 'open-ils.cat.authority.record.import',
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);
37 __PACKAGE__->register_method(
38 method => 'create_authority_record_from_bib_field',
39 api_name => 'open-ils.cat.authority.record.create_from_bib',
41 desc => q/Create an authority record entry from a field in a bibliographic record/,
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
50 __PACKAGE__->register_method(
51 method => 'create_authority_record_from_bib_field',
52 api_name => 'open-ils.cat.authority.record.create_from_bib.readonly',
54 desc => q/Creates MARCXML for an authority record entry from a field in a bibliographic record/,
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
62 sub create_authority_record_from_bib_field {
63 my($self, $conn, $field, $cni, $auth) = @_;
65 # Control number identifier should have been passed in
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'};
75 my $ind1 = $field->{ind1} || ' ';
76 my $ind2 = $field->{ind2} || ' ';
78 my $control = qq{<datafield tag="$tag" ind1="$ind1" ind2="$ind2">};
79 foreach my $sf (@{$field->{subfields}}) {
81 my $val = $U->entityize($sf->[1]);
82 $control .= qq{<subfield code="$code">$val</subfield>};
84 $control .= '</datafield>';
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();
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
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>
107 if ($self->api_name =~ m/readonly$/) {
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);
119 __PACKAGE__->register_method(
120 method => 'overlay_authority_record',
121 api_name => 'open-ils.cat.authority.record.overlay',
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);
135 __PACKAGE__->register_method(
136 method => 'retrieve_authority_record',
137 api_name => 'open-ils.cat.authority.record.retrieve',
139 desc => q/Retrieve an authority record entry/,
141 {desc => q/hash of options. Options include "clear_marc" which clears
142 the MARC xml from the record before it is returned/}
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};
155 __PACKAGE__->register_method(
156 method => 'batch_retrieve_authority_record',
157 api_name => 'open-ils.cat.authority.record.batch.retrieve',
160 desc => q/Retrieve a set of authority record entry objects/,
162 {desc => q/hash of options. Options include "clear_marc" which clears
163 the MARC xml from the record before it is returned/}
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);
179 __PACKAGE__->register_method(
180 method => 'count_linked_bibs',
181 api_name => 'open-ils.cat.authority.records.count_linked_bibs',
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")
189 sub count_linked_bibs {
190 my( $self, $conn, $records ) = @_;
192 my $editor = new_editor();
196 for my $auth ( @$records ) {
197 # Protection against SQL injection? Might be overkill.
198 my $intauth = int($auth);
200 push(@clean_records, $intauth);
203 return $link_count if !@clean_records;
205 $link_count = $editor->json_query({
209 "column" => "authority"
213 "transform" => "count",
220 "where" => { "authority" => \@clean_records }
226 __PACKAGE__->register_method(
227 "method" => "retrieve_acs",
228 "api_name" => "open-ils.cat.authority.control_set.retrieve",
233 "desc" => q/Retrieve authority.control_set objects with fleshed
234 thesauri and authority fields/,
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"}
243 # XXX I don't think this really needs to be protected by perms, or does it?
248 my ($limit, $offset, $focus) = map int, @_;
256 {"class" => "acs", "field" => "name"}
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"
267 my $sets = $e->search_authority_control_set([
268 {"id" => {"!=" => undef}}, {
270 "flesh_fields" => {"acs" => [qw/thesauri authority_fields/]},
271 "order_by" => $order_by,
275 ]) or return $e->die_event;
279 $client->respond($_) foreach @$sets;
283 __PACKAGE__->register_method(
284 "method" => "retrieve_acsaf",
285 "api_name" => "open-ils.cat.authority.control_set_authority_field.retrieve",
290 "desc" => q/Retrieve authority.control_set_authority_field objects with
291 fleshed bib_fields and axes/,
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)"}
305 my ($limit, $offset, $control_set, $focus) = map int, @_;
309 $control_set ||= undef;
314 "control_set" => ($control_set ? $control_set : {"!=" => undef})
317 {"class" => "acsaf", "field" => "main_entry", "direction" => "desc"},
318 {"class" => "acsaf", "field" => "id"}
321 unshift @$order_by, {
322 "class" => "acsaf", "field" => "id",
323 "transform" => "numeric_eq", "params" => [$focus],
324 "direction" => "desc"
327 my $fields = $e->search_authority_control_set_authority_field([
331 "acsaf" => ["bib_fields", "axis_maps"],
334 "order_by" => $order_by,
338 ]) or return $e->die_event;
342 $client->respond($_) foreach @$fields;
346 __PACKAGE__->register_method(
347 method => "bib_field_overlay_authority_field",
348 api_name => "open-ils.cat.authority.bib_field.overlay_authority",
353 desc => q/Given a bib field hash and an authority field hash,
354 merge the authority data for controlled fields into the
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./}
366 return => q/The modified bib field/
370 # Returns the first found field.
371 sub get_auth_field_by_tag {
372 my ($atag, $cset_id) = @_;
374 my $e = new_editor();
376 my $where = {tag => $atag};
378 $where->{control_set} = $cset_id if $cset_id;
380 return $e->search_authority_control_set_authority_field($where)->[0];
383 sub bib_field_overlay_authority_field {
384 my ($self, $client, $bib_field, $auth_field, $cset_id) = @_;
386 return $bib_field unless $bib_field && $auth_field;
388 my $btag = $bib_field->{'tag'};
389 my $atag = $auth_field->{'tag'};
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
397 my $acsaf = get_auth_field_by_tag($atag, $cset_id);
400 $sf_list = $acsaf->sf_list;
405 (my $alt_atag = $atag) =~ s/^./1/;
406 $acsaf = get_auth_field_by_tag($alt_atag, $cset_id) if $alt_atag ne $atag;
408 $sf_list = $acsaf->sf_list if $acsaf;
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
422 } elsif (index($sf_list, $c) > -1) {
423 push(@$subfields, [$c, $v]);
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
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]);
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;
444 my $new_bib_field = {
445 tag => $bib_field->{tag},
446 ind1 => $auth_field->{'ind1'},
447 ind2 => $auth_field->{'ind2'},
448 subfields => $subfields
451 $new_bib_field->{ind1} = $auth_field->{'ind2'}
452 if $atag eq '130' && $btag eq '130';
454 return $new_bib_field;
457 __PACKAGE__->register_method(
458 method => "validate_bib_fields",
459 api_name => "open-ils.cat.authority.validate.bib_field",
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./,
468 {type => 'object', name => 'Bib Fields',
470 List of objects like this
475 subfields: [[code, value], ...]
479 srfsh# request open-ils.cat open-ils.cat.authority.validate.bib_field
480 [{"tag":"600","ind1":"", "ind2":"", "subfields":[["a","shakespeare william"], ...]}]
483 {type => 'number', name => 'Optional Control Set ID'},
488 # for stub records sent to
489 # open-ils.cat.authority.simple_heading
490 my $auth_leader = '00000czm a2200205Ka 4500';
492 sub validate_bib_fields {
493 my ($self, $client, $bib_fields, $control_set) = @_;
495 $bib_fields = [$bib_fields] unless ref $bib_fields eq 'ARRAY';
497 my $e = new_editor();
499 for my $bib_field (@$bib_fields) {
501 $bib_field->{valid} = 0;
503 my $where = {'+acsbf' => {tag => $bib_field->{tag}}};
504 $where->{'+acsaf'} = {control_set => $control_set} if $control_set;
506 my $auth_field_list = $e->json_query({
508 acsbf => ['authority_field'],
509 acsaf => ['id', 'tag', 'sf_list', 'control_set']
511 from => {acsbf => {acsaf => {}}},
514 {class => 'acsaf', field => 'main_entry', direction => 'desc'},
515 {class => 'acsaf', field => 'tag'}
520 for my $auth_field (@$auth_field_list) {
522 my $sf_list = $auth_field->{sf_list};
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;
528 push(@seen_subfields, $sf_list);
531 for my $subfield (@{$bib_field->{subfields}}) {
532 my $code = $subfield->[0];
533 my $value = $subfield->[1];
535 next unless defined $value && $value ne '';
537 # is this a controlled subfield?
538 next unless index($sf_list, $code) > -1;
540 push(@sf_values, $code, $value);
543 next unless @sf_values;
545 my $record = MARC::Record->new;
546 $record->leader($auth_leader);
548 my $field = MARC::Field->new($auth_field->{tag},
549 $bib_field->{ind1}, $bib_field->{ind2}, @sf_values);
551 $record->append_fields($field);
553 my $match = $U->simplereq(
555 'open-ils.search.authority.simple_heading.from_xml',
556 $record->as_xml_record, $control_set);
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};
567 # Present our findings.
568 $client->respond($bib_field);
575 __PACKAGE__->register_method(
576 method => "bib_field_authority_linking_browse",
577 api_name => "open-ils.cat.authority.bib_field.linking_browse",
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.
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'}
597 sub get_heading_string {
601 for my $subfield ($field->subfields) {
602 $heading .= ' --' if index('xyz', $subfield->[0]) > -1;
603 $heading .= ' ' if $heading;
604 $heading .= $subfield->[1];
610 # Turns a MARC::Field into a hash and adds the field's heading string.
614 heading => get_heading_string($field),
616 ind1 => $field->indicator(1),
617 ind2 => $field->indicator(2),
618 subfields => [$field->subfields]
622 sub bib_field_authority_linking_browse {
623 my ($self, $client, $bib_field, $limit, $offset, $thesauri) = @_;
628 my $e = new_editor();
630 return [] unless $bib_field;
632 my $term = join(' ', map {$_->[1]} @{$bib_field->{subfields}});
634 return [] unless $term;
636 my $axis = $e->json_query({
637 select => {abaafm => ['axis']},
638 from => {acsbf => {acsaf => {join => 'abaafm'}}},
639 where => {'+acsbf' => {tag => $bib_field->{tag}}},
641 {class => 'acsaf', field => 'main_entry', direction => 'desc'},
642 {class => 'acsaf', field => 'tag'},
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'}
652 return [] unless $axis && ($axis = $axis->{axis});
654 # See https://bugs.launchpad.net/evergreen/+bug/1403098
655 my $are_ids = $U->simplereq(
657 'open-ils.supercat.authority.browse_center.by_axis.refs',
658 $axis, $term, $offset, $limit, $thesauri);
660 for my $are_id (@$are_ids) {
662 my $are = $e->retrieve_authority_record_entry($are_id);
663 my $rec = MARC::Record->new_from_xml($are->marc, 'UTF-8');
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;
670 authority_id => $are_id,
671 main_heading => hashify_field($main_field),
672 auth_org => $auth_org,
677 for my $also_field ($rec->field('5..')) {
678 push(@{$resp->{see_alsos}}, hashify_field($also_field));
681 for my $from_field ($rec->field('4..')) {
682 push(@{$resp->{see_froms}}, hashify_field($from_field));
685 $client->respond($resp);