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