]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/support-scripts/authority_control_fields.pl.in
Revert "LP#1635737 Use new OpenSRF interval_to_seconds() context"
[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 [B<--days_back>=I<number-of-days>]
514
515 =head1 DESCRIPTION
516
517 For a given set of records:
518
519 =over
520
521 =item * Iterate through the list of fields that are controlled fields
522
523 =item * Iterate through the list of subfields that are controlled for
524 that given field
525
526 =item * Search for a matching authority record for that combination of
527 field + subfield(s)
528
529 =over
530
531 =item * If we find a match, then add a $0 subfield to that field identifying
532 the controlling authority record
533
534 =item * If we do not find a match, then insert a row into an "uncontrolled"
535 table identifying the record ID, field, and subfield(s) that were not controlled
536
537 =back
538
539 =item * Iterate through the list of floating subdivisions
540
541 =over
542
543 =item * If we find a match, then add a $0 subfield to that field identifying
544 the controlling authority record
545
546 =item * If we do not find a match, then insert a row into an "uncontrolled"
547 table identifying the record ID, field, and subfield(s) that were not controlled
548
549 =back
550
551 =item * If we changed the record, update it in the database
552
553 =back
554
555 =head1 OPTIONS
556
557 =over
558
559 =item * B<-c> I<config-file>, B<--configuration>=I<config-file>
560
561 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
562 Defaults to F<@sysconfdir@/opensrf_core.xml>
563
564 =item * B<-r> I<record-ID>, B<--record>=I<record-ID>
565
566 Specifies the bibliographic record ID (found in the C<biblio.record_entry.id>
567 column) of the record to process. This option may be specified more than once
568 to process multiple records in a single run.
569
570 =item * B<-a>, B<--all>
571
572 Specifies that all bibliographic records should be processed. For large
573 databases, this may take an extraordinarily long amount of time.
574
575 =item * B<-r>, B<--refresh>
576
577 Specifies that all authority links should be removed from the target
578 bibliographic record(s).  This will effectively rewrite all authority
579 linking anew.
580
581 =item * B<-s> I<start-ID>, B<--start_id>=I<start-ID>
582
583 Specifies the starting ID of the range of bibliographic records to process.
584 This option is ignored unless it is accompanied by the B<-e> or B<--end_id>
585 option.
586
587 =item * B<-e> I<end-ID>, B<--end_id>=I<end-ID>
588
589 Specifies the ending ID of the range of bibliographic records to process.
590 This option is ignored unless it is accompanied by the B<-s> or B<--start_id>
591 option.
592
593 =item * B<--days_back>=I<number-of-days>
594
595 Specifies that only bibliographic records that have been created in the
596 past few days should be processed.  You must specify how many days back
597 to include.  This option is incompatible with the B<-s> and B<--start_id>
598 options.
599
600 =back
601
602 =head1 EXAMPLES
603
604     authority_control_fields.pl --start_id 1 --end_id 50000
605
606 Processes the bibliographic records with IDs between 1 and 50,000 using the
607 default OpenSRF configuration file for connection information.
608
609 =head1 AUTHOR
610
611 Dan Scott <dscott@laurentian.ca>
612
613 =head1 COPYRIGHT AND LICENSE
614
615 Copyright 2010-2011 by Dan Scott
616
617 This program is free software; you can redistribute it and/or
618 modify it under the terms of the GNU General Public License
619 as published by the Free Software Foundation; either version 2
620 of the License, or (at your option) any later version.
621
622 This program is distributed in the hope that it will be useful,
623 but WITHOUT ANY WARRANTY; without even the implied warranty of
624 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
625 GNU General Public License for more details.
626
627 You should have received a copy of the GNU General Public License
628 along with this program; if not, write to the Free Software
629 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
630
631 =cut
632