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