]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/support-scripts/authority_control_fields.pl
Prevent a MARC::File::XML error from killing authority_control_fields.pl batch
[working/Evergreen.git] / Open-ILS / src / support-scripts / authority_control_fields.pl
1 #!/usr/bin/perl
2 # Copyright (C) 2010 Laurentian University
3 # Author: Dan Scott <dscott@laurentian.ca>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # ---------------------------------------------------------------
15
16 use strict;
17 use warnings;
18 use DBI;
19 use Getopt::Long;
20 use MARC::Record;
21 use MARC::File::XML (BinaryEncoding => 'UTF-8');
22 use OpenSRF::System;
23 use OpenILS::Utils::Fieldmapper;
24 use OpenSRF::Utils::SettingsClient;
25 use OpenSRF::EX qw/:try/;
26 use Encode;
27 use Unicode::Normalize;
28 use OpenILS::Application::AppUtils;
29 use Data::Dumper;
30 use Pod::Usage qw/ pod2usage /;
31
32 my ($start_id, $end_id);
33 my $bootstrap = '/openils/conf/opensrf_core.xml';
34 my @records;
35
36 my %options;
37 my $result = GetOptions(
38     \%options,
39     'configuration=s' => \$bootstrap,
40     'record=i' => \@records,
41     'all', 'help',
42     'start_id=i' => \$start_id,
43     'end_id=i' => \$end_id,
44 );
45
46 if (!$result or $options{help}) {
47     pod2usage(0);
48 }
49
50 OpenSRF::System->bootstrap_client(config_file => $bootstrap);
51 Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
52
53 # must be loaded and initialized after the IDL is parsed
54 use OpenILS::Utils::CStoreEditor;
55 OpenILS::Utils::CStoreEditor::init();
56
57 my $editor = OpenILS::Utils::CStoreEditor->new;
58 my $undeleted;
59 if ($options{all}) {
60     # get a list of all non-deleted records from Evergreen
61     # open-ils.cstore open-ils.cstore.direct.biblio.record_entry.id_list.atomic {"deleted":"f"}
62     $undeleted = $editor->request( 
63         'open-ils.cstore.direct.biblio.record_entry.id_list.atomic', 
64         [{deleted => 'f'}, {id => { '>' => 0}}]
65     );
66     @records = @$undeleted;
67 }
68
69 if ($start_id and $end_id) {
70     @records = ($start_id .. $end_id);
71 }
72 # print Dumper($undeleted, \@records);
73
74 # Hash of controlled fields & subfields in bibliographic records, and their
75 # corresponding controlling fields & subfields in the authority record
76 #
77 # So, if the bib 650$a can be controlled by an auth 150$a, that maps to:
78 # 650 => { a => { 150 => 'a'}}
79 my %controllees = (
80     100 => { a => { 100 => 'a' },
81              b => { 100 => 'b' },
82              c => { 100 => 'c' },
83              d => { 100 => 'd' },
84              e => { 100 => 'e' },
85              f => { 100 => 'f' },
86              g => { 100 => 'g' },
87              j => { 100 => 'j' },
88              k => { 100 => 'k' },
89              l => { 100 => 'l' },
90              n => { 100 => 'n' },
91              p => { 100 => 'p' },
92              q => { 100 => 'q' },
93              t => { 100 => 't' },
94              u => { 100 => 'u' },
95              4 => { 100 => '4' },
96     },
97     110 => { a => { 110 => 'a' },
98              b => { 110 => 'b' },
99              c => { 110 => 'c' },
100              d => { 110 => 'd' },
101              e => { 110 => 'e' },
102              f => { 110 => 'f' },
103              g => { 110 => 'g' },
104              k => { 110 => 'k' },
105              l => { 110 => 'l' },
106              n => { 110 => 'n' },
107              p => { 110 => 'p' },
108              t => { 110 => 't' },
109              u => { 110 => 'u' },
110              4 => { 110 => '4' },
111     },
112     111 => { a => { 111 => 'a' },
113              c => { 111 => 'c' },
114              d => { 111 => 'd' },
115              e => { 111 => 'e' },
116              f => { 111 => 'f' },
117              g => { 111 => 'g' },
118              j => { 111 => 'j' },
119              k => { 111 => 'k' },
120              l => { 111 => 'l' },
121              n => { 111 => 'n' },
122              p => { 111 => 'p' },
123              q => { 111 => 'q' },
124              t => { 111 => 't' },
125              u => { 111 => 'u' },
126              4 => { 111 => '4' },
127     },
128     130 => { a => { 130 => 'a' },
129              d => { 130 => 'd' },
130              f => { 130 => 'f' },
131              g => { 130 => 'g' },
132              h => { 130 => 'h' },
133              k => { 130 => 'k' },
134              l => { 130 => 'l' },
135              m => { 130 => 'm' },
136              n => { 130 => 'n' },
137              o => { 130 => 'o' },
138              p => { 130 => 'p' },
139              r => { 130 => 'r' },
140              s => { 130 => 's' },
141              t => { 130 => 't' },
142     },
143     600 => { a => { 100 => 'a' },
144              b => { 100 => 'b' },
145              c => { 100 => 'c' },
146              d => { 100 => 'd' },
147              e => { 100 => 'e' },
148              f => { 100 => 'f' },
149              g => { 100 => 'g' },
150              h => { 100 => 'h' },
151              j => { 100 => 'j' },
152              k => { 100 => 'k' },
153              l => { 100 => 'l' },
154              m => { 100 => 'm' },
155              n => { 100 => 'n' },
156              o => { 100 => 'o' },
157              p => { 100 => 'p' },
158              q => { 100 => 'q' },
159              r => { 100 => 'r' },
160              s => { 100 => 's' },
161              t => { 100 => 't' },
162              v => { 100 => 'v' },
163              x => { 100 => 'x' },
164              y => { 100 => 'y' },
165              z => { 100 => 'z' },
166              4 => { 100 => '4' },
167     },
168     610 => { a => { 110 => 'a' },
169              b => { 110 => 'b' },
170              c => { 110 => 'c' },
171              d => { 110 => 'd' },
172              e => { 110 => 'e' },
173              f => { 110 => 'f' },
174              g => { 110 => 'g' },
175              h => { 110 => 'h' },
176              k => { 110 => 'k' },
177              l => { 110 => 'l' },
178              m => { 110 => 'm' },
179              n => { 110 => 'n' },
180              o => { 110 => 'o' },
181              p => { 110 => 'p' },
182              r => { 110 => 'r' },
183              s => { 110 => 's' },
184              t => { 110 => 't' },
185              v => { 110 => 'v' },
186              x => { 110 => 'x' },
187              y => { 110 => 'y' },
188              z => { 110 => 'z' },
189     },
190     611 => { a => { 111 => 'a' },
191              c => { 111 => 'c' },
192              d => { 111 => 'd' },
193              e => { 111 => 'e' },
194              f => { 111 => 'f' },
195              g => { 111 => 'g' },
196              h => { 111 => 'h' },
197              j => { 111 => 'j' },
198              k => { 111 => 'k' },
199              l => { 111 => 'l' },
200              n => { 111 => 'n' },
201              p => { 111 => 'p' },
202              q => { 111 => 'q' },
203              s => { 111 => 's' },
204              t => { 111 => 't' },
205              v => { 111 => 'v' },
206              x => { 111 => 'x' },
207              y => { 111 => 'y' },
208              z => { 111 => 'z' },
209     },
210     630 => { a => { 130 => 'a' },
211              d => { 130 => 'd' },
212              f => { 130 => 'f' },
213              g => { 130 => 'g' },
214              h => { 130 => 'h' },
215              k => { 130 => 'k' },
216              l => { 130 => 'l' },
217              m => { 130 => 'm' },
218              n => { 130 => 'n' },
219              o => { 130 => 'o' },
220              p => { 130 => 'p' },
221              r => { 130 => 'r' },
222              s => { 130 => 's' },
223              t => { 130 => 't' },
224              v => { 130 => 'v' },
225              x => { 130 => 'x' },
226              y => { 130 => 'y' },
227              z => { 130 => 'z' },
228     },
229     648 => { a => { 148 => 'a' },
230              v => { 148 => 'v' },
231              x => { 148 => 'x' },
232              y => { 148 => 'y' },
233              z => { 148 => 'z' },
234     },
235     650 => { a => { 150 => 'a' },
236              b => { 150 => 'b' },
237              v => { 150 => 'v' },
238              x => { 150 => 'x' },
239              y => { 150 => 'y' },
240              z => { 150 => 'z' },
241     },
242     651 => { a => { 151 => 'a' },
243              v => { 151 => 'v' },
244              x => { 151 => 'x' },
245              y => { 151 => 'y' },
246              z => { 151 => 'z' },
247     },
248     655 => { a => { 155 => 'a' },
249              v => { 155 => 'v' },
250              x => { 155 => 'x' },
251              y => { 155 => 'y' },
252              z => { 155 => 'z' },
253     },
254     700 => { a => { 100 => 'a' },
255              b => { 100 => 'b' },
256              c => { 100 => 'c' },
257              d => { 100 => 'd' },
258              e => { 100 => 'e' },
259              f => { 100 => 'f' },
260              g => { 100 => 'g' },
261              j => { 100 => 'j' },
262              k => { 100 => 'k' },
263              l => { 100 => 'l' },
264              n => { 100 => 'n' },
265              p => { 100 => 'p' },
266              q => { 100 => 'q' },
267              t => { 100 => 't' },
268              u => { 100 => 'u' },
269              4 => { 100 => '4' },
270     },
271     710 => { a => { 110 => 'a' },
272              b => { 110 => 'b' },
273              c => { 110 => 'c' },
274              d => { 110 => 'd' },
275              e => { 110 => 'e' },
276              f => { 110 => 'f' },
277              g => { 110 => 'g' },
278              k => { 110 => 'k' },
279              l => { 110 => 'l' },
280              n => { 110 => 'n' },
281              p => { 110 => 'p' },
282              t => { 110 => 't' },
283              u => { 110 => 'u' },
284              4 => { 110 => '4' },
285     },
286     711 => { a => { 111 => 'a' },
287              c => { 111 => 'c' },
288              d => { 111 => 'd' },
289              e => { 111 => 'e' },
290              f => { 111 => 'f' },
291              g => { 111 => 'g' },
292              j => { 111 => 'j' },
293              k => { 111 => 'k' },
294              l => { 111 => 'l' },
295              n => { 111 => 'n' },
296              p => { 111 => 'p' },
297              q => { 111 => 'q' },
298              t => { 111 => 't' },
299              u => { 111 => 'u' },
300              4 => { 111 => '4' },
301     },
302     730 => { a => { 130 => 'a' },
303              d => { 130 => 'd' },
304              f => { 130 => 'f' },
305              g => { 130 => 'g' },
306              h => { 130 => 'h' },
307              k => { 130 => 'k' },
308              l => { 130 => 'l' },
309              m => { 130 => 'm' },
310              n => { 130 => 'n' },
311              o => { 130 => 'o' },
312              p => { 130 => 'p' },
313              r => { 130 => 'r' },
314              s => { 130 => 's' },
315              t => { 130 => 't' },
316     },
317     751 => { a => { 151 => 'a' },
318              v => { 151 => 'v' },
319              x => { 151 => 'x' },
320              y => { 151 => 'y' },
321              z => { 151 => 'z' },
322     },
323     830 => { a => { 830 => 'a' },
324              d => { 130 => 'd' },
325              f => { 130 => 'f' },
326              g => { 130 => 'g' },
327              h => { 130 => 'h' },
328              k => { 130 => 'k' },
329              l => { 130 => 'l' },
330              m => { 130 => 'm' },
331              n => { 130 => 'n' },
332              o => { 130 => 'o' },
333              p => { 130 => 'p' },
334              r => { 130 => 'r' },
335              s => { 130 => 's' },
336              t => { 130 => 't' },
337     },
338 );
339
340 foreach my $rec_id (@records) {
341     # print "$rec_id\n";
342
343     my $e = OpenILS::Utils::CStoreEditor->new();
344     # State variable; was the record changed?
345     my $changed;
346
347     # get the record
348     my $record = $e->retrieve_biblio_record_entry($rec_id);
349     next unless $record;
350     # print Dumper($record);
351
352     try {
353         my $marc = MARC::Record->new_from_xml($record->marc());
354
355         # get the list of controlled fields
356         my @c_fields = keys %controllees;
357
358         foreach my $c_tag (@c_fields) {
359             my @c_subfields = keys %{$controllees{"$c_tag"}};
360             # print "Field: $field subfields: ";
361             # foreach (@subfields) { print "$_ "; }
362
363             # Get the MARCXML from the record and check for controlled fields/subfields
364             my @bib_fields = ($marc->field($c_tag));
365             foreach my $bib_field (@bib_fields) {
366                 # print $_->as_formatted(); 
367                 my %match_subfields;
368                 my $match_tag;
369                 my @searches;
370                 foreach my $c_subfield (@c_subfields) {
371                     my $sf = $bib_field->subfield($c_subfield);
372                     if ($sf) {
373                         # Give me the first element of the list of authority controlling tags for this subfield
374                         # XXX Will we need to support more than one controlling tag per subfield? Probably. That
375                         # will suck. Oh well, leave that up to Ole to implement.
376                         $match_subfields{$c_subfield} = (keys %{$controllees{$c_tag}{$c_subfield}})[0];
377                         $match_tag = $match_subfields{$c_subfield};
378                         push @searches, {term => $sf, subfield => $c_subfield};
379                     }
380                 }
381                 # print Dumper(\%match_subfields);
382                 next if !$match_tag;
383
384                 my @tags = ($match_tag);
385
386                 # print "Controlling tag: $c_tag and match tag $match_tag\n";
387                 # print Dumper(\@tags, \@searches);
388
389                 # Now we've built up a complete set of matching controlled
390                 # subfields for this particular field; let's check to see if
391                 # we have a matching authority record
392                 my $session = OpenSRF::AppSession->create("open-ils.search");
393                 my $validates = $session->request("open-ils.search.authority.validate.tag.id_list", 
394                     "tags", \@tags, "searches", \@searches
395                 )->gather();
396                 $session->disconnect();
397
398                 # print Dumper($validates);
399
400                 # Protect against failed (error condition) search request
401                 if (!$validates) {
402                     print STDERR "Search for matching authority failed; record # $rec_id\n";
403                     next;
404                 }
405
406                 if (scalar(@$validates) == 0) {
407                     next;
408                 }
409
410                 # Iterate through the returned authority record IDs to delete any
411                 # matching $0 subfields already in the bib record
412                 foreach my $auth_zero (@$validates) {
413                     $bib_field->delete_subfield(code => '0', match => qr/\)$auth_zero$/);
414                 }
415
416                 # Okay, we have a matching authority control; time to
417                 # add the magical subfield 0. Use the first returned auth
418                 # record as a match.
419                 my $auth_id = @$validates[0];
420                 my $auth_rec = $e->retrieve_authority_record_entry($auth_id);
421                 my $auth_marc = MARC::Record->new_from_xml($auth_rec->marc());
422                 my $cni = $auth_marc->field('003')->data();
423                 
424                 $bib_field->add_subfields('0' => "($cni)$auth_id");
425                 $changed = 1;
426             }
427         }
428         if ($changed) {
429             my $editor = OpenILS::Utils::CStoreEditor->new(xact=>1);
430             # print $marc->as_formatted();
431             my $xml = $marc->as_xml_record();
432             $xml =~ s/\n//sgo;
433             $xml =~ s/^<\?xml.+\?\s*>//go;
434             $xml =~ s/>\s+</></go;
435             $xml =~ s/\p{Cc}//go;
436             $xml = OpenILS::Application::AppUtils->entityize($xml);
437
438             $record->marc($xml);
439             $editor->update_biblio_record_entry($record);
440             $editor->commit();
441         }
442     } otherwise {
443         my $err = shift;
444         print STDERR "\nRecord # $rec_id : $err\n";
445         import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire process
446     }
447 }
448
449 __END__
450
451 =head1 NAME
452
453 authority_control_fields.pl - Controls fields in bibliographic records with authorities in Evergreen
454
455 =head1 SYNOPSIS
456
457 C<authority_control_fields.pl> [B<--configuration>=I<opensrf_core.conf>]
458 [[B<--record>=I<record>[ B<--record>=I<record>]]] | [B<--all>] | [B<--start_id>=I<start-ID> B<--end_id>=I<end-ID>]
459
460 =head1 DESCRIPTION
461
462 For a given set of records:
463
464 =over
465
466 =item * Iterate through the list of fields that are controlled fields
467
468 =item * Iterate through the list of subfields that are controlled for
469 that given field
470
471 =item * Search for a matching authority record for that combination of
472 field + subfield(s)
473
474 =over
475
476 =item * If we find a match, then add a $0 subfield to that field identifying
477 the controlling authority record
478
479 =item * If we do not find a match, then insert a row into an "uncontrolled"
480 table identifying the record ID, field, and subfield(s) that were not controlled
481
482 =back
483
484 =item * Iterate through the list of floating subdivisions
485
486 =over
487
488 =item * If we find a match, then add a $0 subfield to that field identifying
489 the controlling authority record
490
491 =item * If we do not find a match, then insert a row into an "uncontrolled"
492 table identifying the record ID, field, and subfield(s) that were not controlled
493
494 =back
495
496 =item * If we changed the record, update it in the database
497
498 =back
499
500 =head1 OPTIONS
501
502 =over
503
504 =item * B<-c> I<config-file>, B<--configuration>=I<config-file>
505
506 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
507 Defaults to F</openils/conf/opensrf_core.xml>
508
509 =item * B<-r> I<record-ID>, B<--record>=I<record-ID>
510
511 Specifies the bibliographic record ID (found in the C<biblio.record_entry.id>
512 column) of the record to process. This option may be specified more than once
513 to process multiple records in a single run.
514
515 =item * B<-a>, B<--all>
516
517 Specifies that all bibliographic records should be processed. For large
518 databases, this may take an extraordinarily long amount of time.
519
520 =item * B<-s> I<start-ID>, B<--start_id>=I<start-ID>
521
522 Specifies the starting ID of the range of bibliographic records to process.
523 This option is ignored unless it is accompanied by the B<-e> or B<--end_id>
524 option.
525
526 =item * B<-e> I<end-ID>, B<--end_id>=I<end-ID>
527
528 Specifies the ending ID of the range of bibliographic records to process.
529 This option is ignored unless it is accompanied by the B<-s> or B<--start>
530 option.
531
532 =back
533
534 =head1 EXAMPLES
535
536     authority_control_fields.pl --start_id 1 --end_id 50000
537
538 Processes the bibliographic records with IDs between 1 and 50,000 using the
539 default OpenSRF configuration file for connection information.
540
541 =head1 AUTHOR
542
543 Dan Scott <dscott@laurentian.ca>
544
545 =head1 COPYRIGHT AND LICENSE
546
547 Copyright 2010-2011 by Dan Scott
548
549 This program is free software; you can redistribute it and/or
550 modify it under the terms of the GNU General Public License
551 as published by the Free Software Foundation; either version 2
552 of the License, or (at your option) any later version.
553
554 This program is distributed in the hope that it will be useful,
555 but WITHOUT ANY WARRANTY; without even the implied warranty of
556 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
557 GNU General Public License for more details.
558
559 You should have received a copy of the GNU General Public License
560 along with this program; if not, write to the Free Software
561 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
562
563 =cut
564