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