07c9f77f10f13c928055107daa0baa727a9d6acc
[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 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 = '@sysconfdir@/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 $e = 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 = $e->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     # State variable; was the record changed?
344     my $changed;
345
346     # get the record
347     my $record = $e->retrieve_biblio_record_entry($rec_id);
348     next unless $record;
349     # print Dumper($record);
350
351     try {
352         my $marc = MARC::Record->new_from_xml($record->marc());
353
354         # get the list of controlled fields
355         my @c_fields = keys %controllees;
356
357         foreach my $c_tag (@c_fields) {
358             my @c_subfields = keys %{$controllees{"$c_tag"}};
359             # print "Field: $field subfields: ";
360             # foreach (@subfields) { print "$_ "; }
361
362             # Get the MARCXML from the record and check for controlled fields/subfields
363             my @bib_fields = ($marc->field($c_tag));
364             foreach my $bib_field (@bib_fields) {
365                 # print $_->as_formatted(); 
366                 my %match_subfields;
367                 my $match_tag;
368                 my @searches;
369                 foreach my $c_subfield (@c_subfields) {
370                     my $sf = $bib_field->subfield($c_subfield);
371                     if ($sf) {
372                         # Give me the first element of the list of authority controlling tags for this subfield
373                         # XXX Will we need to support more than one controlling tag per subfield? Probably. That
374                         # will suck. Oh well, leave that up to Ole to implement.
375                         $match_subfields{$c_subfield} = (keys %{$controllees{$c_tag}{$c_subfield}})[0];
376                         $match_tag = $match_subfields{$c_subfield};
377                         push @searches, {term => $sf, subfield => $c_subfield};
378                     }
379                 }
380                 # print Dumper(\%match_subfields);
381                 next if !$match_tag;
382
383                 my @tags = ($match_tag);
384
385                 # print "Controlling tag: $c_tag and match tag $match_tag\n";
386                 # print Dumper(\@tags, \@searches);
387
388                 # Now we've built up a complete set of matching controlled
389                 # subfields for this particular field; let's check to see if
390                 # we have a matching authority record
391                 my $session = OpenSRF::AppSession->create("open-ils.search");
392                 my $validates = $session->request("open-ils.search.authority.validate.tag.id_list", 
393                     "tags", \@tags, "searches", \@searches
394                 )->gather();
395                 $session->disconnect();
396
397                 # print Dumper($validates);
398
399                 # Protect against failed (error condition) search request
400                 if (!$validates) {
401                     print STDERR "Search for matching authority failed; record # $rec_id\n";
402                     next;
403                 }
404
405                 if (scalar(@$validates) == 0) {
406                     next;
407                 }
408
409                 # Iterate through the returned authority record IDs to delete any
410                 # matching $0 subfields already in the bib record
411                 foreach my $auth_zero (@$validates) {
412                     $bib_field->delete_subfield(code => '0', match => qr/\)$auth_zero$/);
413                 }
414
415                 # Okay, we have a matching authority control; time to
416                 # add the magical subfield 0. Use the first returned auth
417                 # record as a match.
418                 my $auth_id = @$validates[0];
419                 my $auth_rec = $e->retrieve_authority_record_entry($auth_id);
420                 my $auth_marc = MARC::Record->new_from_xml($auth_rec->marc());
421                 my $cni = $auth_marc->field('003')->data();
422                 
423                 $bib_field->add_subfields('0' => "($cni)$auth_id");
424                 $changed = 1;
425             }
426         }
427         if ($changed) {
428             my $editor = OpenILS::Utils::CStoreEditor->new(xact=>1);
429             # print $marc->as_formatted();
430             my $xml = $marc->as_xml_record();
431             $xml =~ s/\n//sgo;
432             $xml =~ s/^<\?xml.+\?\s*>//go;
433             $xml =~ s/>\s+</></go;
434             $xml =~ s/\p{Cc}//go;
435             $xml = OpenILS::Application::AppUtils->entityize($xml);
436
437             $record->marc($xml);
438             $editor->update_biblio_record_entry($record);
439             $editor->commit();
440         }
441     } otherwise {
442         my $err = shift;
443         print STDERR "\nRecord # $rec_id : $err\n";
444         import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire process
445     }
446 }
447
448 __END__
449
450 =head1 NAME
451
452 authority_control_fields.pl - Controls fields in bibliographic records with authorities in Evergreen
453
454 =head1 SYNOPSIS
455
456 C<authority_control_fields.pl> [B<--configuration>=I<opensrf_core.conf>]
457 [[B<--record>=I<record>[ B<--record>=I<record>]]] | [B<--all>] | [B<--start_id>=I<start-ID> B<--end_id>=I<end-ID>]
458
459 =head1 DESCRIPTION
460
461 For a given set of records:
462
463 =over
464
465 =item * Iterate through the list of fields that are controlled fields
466
467 =item * Iterate through the list of subfields that are controlled for
468 that given field
469
470 =item * Search for a matching authority record for that combination of
471 field + subfield(s)
472
473 =over
474
475 =item * If we find a match, then add a $0 subfield to that field identifying
476 the controlling authority record
477
478 =item * If we do not find a match, then insert a row into an "uncontrolled"
479 table identifying the record ID, field, and subfield(s) that were not controlled
480
481 =back
482
483 =item * Iterate through the list of floating subdivisions
484
485 =over
486
487 =item * If we find a match, then add a $0 subfield to that field identifying
488 the controlling authority record
489
490 =item * If we do not find a match, then insert a row into an "uncontrolled"
491 table identifying the record ID, field, and subfield(s) that were not controlled
492
493 =back
494
495 =item * If we changed the record, update it in the database
496
497 =back
498
499 =head1 OPTIONS
500
501 =over
502
503 =item * B<-c> I<config-file>, B<--configuration>=I<config-file>
504
505 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
506 Defaults to F<@sysconfdir@/opensrf_core.xml>
507
508 =item * B<-r> I<record-ID>, B<--record>=I<record-ID>
509
510 Specifies the bibliographic record ID (found in the C<biblio.record_entry.id>
511 column) of the record to process. This option may be specified more than once
512 to process multiple records in a single run.
513
514 =item * B<-a>, B<--all>
515
516 Specifies that all bibliographic records should be processed. For large
517 databases, this may take an extraordinarily long amount of time.
518
519 =item * B<-s> I<start-ID>, B<--start_id>=I<start-ID>
520
521 Specifies the starting ID of the range of bibliographic records to process.
522 This option is ignored unless it is accompanied by the B<-e> or B<--end_id>
523 option.
524
525 =item * B<-e> I<end-ID>, B<--end_id>=I<end-ID>
526
527 Specifies the ending ID of the range of bibliographic records to process.
528 This option is ignored unless it is accompanied by the B<-s> or B<--start>
529 option.
530
531 =back
532
533 =head1 EXAMPLES
534
535     authority_control_fields.pl --start_id 1 --end_id 50000
536
537 Processes the bibliographic records with IDs between 1 and 50,000 using the
538 default OpenSRF configuration file for connection information.
539
540 =head1 AUTHOR
541
542 Dan Scott <dscott@laurentian.ca>
543
544 =head1 COPYRIGHT AND LICENSE
545
546 Copyright 2010-2011 by Dan Scott
547
548 This program is free software; you can redistribute it and/or
549 modify it under the terms of the GNU General Public License
550 as published by the Free Software Foundation; either version 2
551 of the License, or (at your option) any later version.
552
553 This program is distributed in the hope that it will be useful,
554 but WITHOUT ANY WARRANTY; without even the implied warranty of
555 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
556 GNU General Public License for more details.
557
558 You should have received a copy of the GNU General Public License
559 along with this program; if not, write to the Free Software
560 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
561
562 =cut
563