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