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