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