2 # Copyright (C) 2010-2011 Laurentian University
3 # Author: Dan Scott <dscott@laurentian.ca>
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.
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 # ---------------------------------------------------------------
21 use MARC::File::XML (BinaryEncoding => 'UTF-8');
24 use OpenILS::Utils::Fieldmapper;
25 use OpenSRF::Utils::SettingsClient;
26 use OpenSRF::EX qw/:try/;
28 use Unicode::Normalize;
29 use OpenILS::Application::AppUtils;
31 use Pod::Usage qw/ pod2usage /;
33 MARC::Charset->assume_unicode(1);
35 my ($start_id, $end_id, $refresh);
37 my $bootstrap = '@sysconfdir@/opensrf_core.xml';
41 my $result = GetOptions(
43 'configuration=s' => \$bootstrap,
44 'record=i' => \@records,
45 'refresh' => \$refresh,
47 'start_id=i' => \$start_id,
48 'end_id=i' => \$end_id,
49 'days_back=i' => \$days_back,
52 if (!$result or $options{help}) {
56 if ($start_id && $days_back) {
57 print "Can't use both start ID and days back!\n";
61 OpenSRF::System->bootstrap_client(config_file => $bootstrap);
62 Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
64 # must be loaded and initialized after the IDL is parsed
65 use OpenILS::Utils::CStoreEditor;
66 OpenILS::Utils::CStoreEditor::init();
68 my $e = OpenILS::Utils::CStoreEditor->new;
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}}]
77 @records = @$undeleted;
80 if ($start_id and $end_id) {
81 @records = ($start_id .. $end_id);
84 if (defined $days_back) {
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' );
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;
97 my $db_user = $sc->config_value( reporter => setup => database => 'user' );
98 my $db_pw = $sc->config_value( reporter => setup => database => 'pw' );
100 die "Unable to retrieve database connection information from the settings server" unless ($db_driver && $db_host && $db_port && $db_name && $db_user);
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";
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)))");
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);
116 # print Dumper($undeleted, \@records);
118 # Hash of controlled fields & subfields in bibliographic records, and their
119 # corresponding controlling fields & subfields in the authority record
121 # So, if the bib 650$a can be controlled by an auth 150$a, that maps to:
122 # 650 => { a => { 150 => 'a'}}
124 100 => { a => { 100 => 'a' },
139 110 => { a => { 110 => 'a' },
152 111 => { a => { 111 => 'a' },
167 130 => { a => { 130 => 'a' },
182 600 => { a => { 100 => 'a' },
205 610 => { a => { 110 => 'a' },
226 611 => { a => { 111 => 'a' },
246 630 => { a => { 130 => 'a' },
265 648 => { a => { 148 => 'a' },
271 650 => { a => { 150 => 'a' },
278 651 => { a => { 151 => 'a' },
284 655 => { a => { 155 => 'a' },
290 700 => { a => { 100 => 'a' },
305 710 => { a => { 110 => 'a' },
318 711 => { a => { 111 => 'a' },
333 730 => { a => { 130 => 'a' },
348 751 => { a => { 151 => 'a' },
354 830 => { a => { 130 => 'a' },
371 foreach my $rec_id (@records) {
374 # State variable; was the record changed?
378 my $record = $e->retrieve_biblio_record_entry($rec_id);
380 # print Dumper($record);
383 my $marc = MARC::Record->new_from_xml($record->marc());
385 # get the list of controlled fields
386 my @c_fields = keys %controllees;
388 foreach my $c_tag (@c_fields) {
389 my @c_subfields = keys %{$controllees{"$c_tag"}};
390 # print "Field: $field subfields: ";
391 # foreach (@subfields) { print "$_ "; }
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();
398 if ($refresh and defined(scalar($bib_field->subfield('0')))) {
399 $bib_field->delete_subfield(code => '0');
406 foreach my $c_subfield (@c_subfields) {
407 my @sf_values = $bib_field->subfield($c_subfield);
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;
417 # print Dumper(\%match_subfields);
420 my @tags = ($match_tag);
422 # print "Controlling tag: $c_tag and match tag $match_tag\n";
423 # print Dumper(\@tags, \@searches);
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
432 $session->disconnect();
434 # print Dumper($validates);
436 # Protect against failed (error condition) search request
438 print STDERR "Search for matching authority failed; record # $rec_id\n";
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) {
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$/);
452 # Okay, we have a matching authority control; time to
453 # add the magical subfield 0. Use the first returned auth
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();
460 $bib_field->add_subfields('0' => "($cni)$auth_id");
466 my $editor = OpenILS::Utils::CStoreEditor->new(xact=>1);
467 # print $marc->as_formatted();
468 my $xml = $marc->as_xml_record();
470 $xml =~ s/^<\?xml.+\?\s*>//go;
471 $xml =~ s/>\s+</></go;
472 $xml =~ s/\p{Cc}//go;
473 $xml = OpenILS::Application::AppUtils->entityize($xml);
476 $editor->update_biblio_record_entry($record);
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
490 authority_control_fields.pl - Controls fields in bibliographic records with authorities in Evergreen
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>]
499 For a given set of records:
503 =item * Iterate through the list of fields that are controlled fields
505 =item * Iterate through the list of subfields that are controlled for
508 =item * Search for a matching authority record for that combination of
513 =item * If we find a match, then add a $0 subfield to that field identifying
514 the controlling authority record
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
521 =item * Iterate through the list of floating subdivisions
525 =item * If we find a match, then add a $0 subfield to that field identifying
526 the controlling authority record
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
533 =item * If we changed the record, update it in the database
541 =item * B<-c> I<config-file>, B<--configuration>=I<config-file>
543 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
544 Defaults to F<@sysconfdir@/opensrf_core.xml>
546 =item * B<-r> I<record-ID>, B<--record>=I<record-ID>
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.
552 =item * B<-a>, B<--all>
554 Specifies that all bibliographic records should be processed. For large
555 databases, this may take an extraordinarily long amount of time.
557 =item * B<-r>, B<--refresh>
559 Specifies that all authority links should be removed from the target
560 bibliographic record(s). This will effectively rewrite all authority
563 =item * B<-s> I<start-ID>, B<--start_id>=I<start-ID>
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>
569 =item * B<-e> I<end-ID>, B<--end_id>=I<end-ID>
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>
579 authority_control_fields.pl --start_id 1 --end_id 50000
581 Processes the bibliographic records with IDs between 1 and 50,000 using the
582 default OpenSRF configuration file for connection information.
586 Dan Scott <dscott@laurentian.ca>
588 =head1 COPYRIGHT AND LICENSE
590 Copyright 2010-2011 by Dan Scott
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.
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.
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.