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