Overhaul the marc2sre.pl script for importing MFHD records
[evergreen-equinox.git] / Open-ILS / src / extras / import / marc2sre.pl
1 #!/usr/bin/perl
2 use strict;
3 use warnings;
4
5 use OpenSRF::System;
6 use OpenSRF::EX qw/:try/;
7 use OpenSRF::Utils::SettingsClient;
8 use OpenILS::Application::AppUtils;
9 use OpenILS::Event;
10 use OpenILS::Utils::Fieldmapper;
11 use OpenSRF::Utils::JSON;
12 use Unicode::Normalize;
13
14 use Time::HiRes qw/time/;
15 use Getopt::Long;
16 use MARC::Batch;
17 use MARC::File::XML ( BinaryEncoding => 'utf-8' );
18 use MARC::Charset;
19 use Pod::Usage;
20
21 MARC::Charset->ignore_errors(1);
22
23 # Command line options, with applicable defaults
24 my ($idsubfield, $bibfield, $bibsubfield, @files, $libmap, $quiet, $help);
25 my $idfield = '004';
26 my $count = 1;
27 my $user = 'admin';
28 my $config = '/openils/conf/opensrf_core.xml';
29 my $marctype = 'USMARC';
30
31 my $parse_options = GetOptions(
32         'idfield=s'     => \$idfield,
33         'idsubfield=s'  => \$idsubfield,
34     'bibfield=s'=> \$bibfield,
35     'bibsubfield=s'=> \$bibsubfield,
36         'startid=i'     => \$count,
37         'user=s'        => \$user,
38         'config=s'      => \$config,
39         'marctype=s'    => \$marctype,
40         'file=s'        => \@files,
41         'libmap=s'      => \$libmap,
42         'quiet'         => \$quiet,
43         'help'          => \$help,
44 );
45
46 if (!$parse_options or $help) {
47     pod2usage(0);
48 }
49
50 @files = @ARGV if (!@files);
51
52 my $U = 'OpenILS::Application::AppUtils';
53 my @ses;
54 my @req;
55 my %processing_cache;
56 my $lib_id_map;
57 if ($libmap) {
58         $lib_id_map = map_libraries_to_ID($libmap);
59 }
60
61 OpenSRF::System->bootstrap_client( config_file => $config );
62 Fieldmapper->import(IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
63
64 my ($result, $evt) = get_user_id($user);
65 if ($evt || !$result->id) {
66     print("Could not retrieve user with user name '$user'\n");
67     exit(0);
68 }
69
70 $user = $result->id;
71
72 select STDERR; $| = 1;
73 select STDOUT; $| = 1;
74
75 my $batch = new MARC::Batch ( $marctype, @files );
76 $batch->strict_off();
77 $batch->warnings_off();
78
79 my $starttime = time;
80 my $rec;
81 while ( try { $rec = $batch->next } otherwise { $rec = -1 } ) {
82         next if ($rec == -1);
83         my $id = $count;
84         my $record_field;
85         if ($idsubfield) {
86                 $record_field = $rec->field($idfield, $idsubfield);
87         } else {
88                 $record_field = $rec->field($idfield);
89         }
90         my $record = $count;
91
92         # On some systems, the 001 actually points to the record ID
93         # We need to attach to the call number to handle holdings in different libraries
94         # but we can work out call numbers later in SQL by the record ID + call number text
95         if ($record_field) {
96                 $record = $record_field->data;
97                 $record =~ s/^.*?(\d+).*?$/$1/o;
98         }
99
100     # If we have been given bibfield / bibsubfield values, use those to find
101     # a matching bib record for $record and use _that_ as our record instead
102     if ($bibfield) {
103         my ($result, $evt) = map_id_to_bib($record);
104         if ($evt || !$result->record) {
105             print("Could not find matching bibliographic record for $record\n");
106         }
107         $record = $result->record;
108     }
109
110         (my $xml = $rec->as_xml_record()) =~ s/\n//sog;
111         $xml =~ s/^<\?xml.+\?\s*>//go;
112         $xml =~ s/>\s+</></go;
113         $xml =~ s/\p{Cc}//go;
114         $xml = OpenILS::Application::AppUtils->entityize($xml);
115         $xml =~ s/[\x00-\x1f]//go;
116
117         my $bib = new Fieldmapper::serial::record_entry;
118         $bib->id($id);
119         $bib->record($record);
120         $bib->active('t');
121         $bib->deleted('f');
122         $bib->marc($xml);
123         $bib->creator($user);
124         $bib->create_date('now');
125         $bib->editor($user);
126         $bib->edit_date('now');
127         $bib->last_xact_id('IMPORT-'.$starttime);
128
129         if ($libmap) {
130                 my $lib_id = get_library_id($rec);
131                 if ($lib_id) {
132                         $bib->owning_lib($lib_id);
133                 }
134         }
135
136         print OpenSRF::Utils::JSON->perl2JSON($bib)."\n";
137
138         $count++;
139
140         if (!$quiet && !($count % 20)) {
141                 print STDERR "\r$count\t". $count / (time - $starttime);
142         }
143 }
144
145 # Generate a hash of library names (as found in the 852b in the MFHD record) to
146 # integers representing actor.org_unit ID values
147 sub map_libraries_to_ID {
148         my $map_filename = shift;
149
150         my %lib_id_map;
151
152         open(MAP_FH, '<', $map_filename) or die "Could not load [$map_filename] $!";
153         while (<MAP_FH>) {
154                 my ($lib, $id) = $_ =~ /^(.*?)\t(.*?)$/;
155                 $lib_id_map{$lib} = $id;
156         }
157
158         return \%lib_id_map;
159 }
160
161 # Look up the actor.org_unit.id value for this library name
162 sub get_library_id {
163         my $record = shift;
164
165         my $lib_name = $record->field('852')->subfield('b');
166         my $lib_id = $lib_id_map->{$lib_name};
167
168         return $lib_id;
169 }
170
171 # Get the actor.usr.id value for the given username
172 sub get_user_id {
173     my $username = shift;
174
175     my ($result, $evt);
176
177     $result = $U->cstorereq(
178         'open-ils.cstore.direct.actor.user.search',
179         { usrname => $username, deleted => 'f' }
180     );
181     $evt = OpenILS::Event->new('ACTOR_USR_NOT_FOUND') unless $result;
182
183     return ($result, $evt);
184 }
185
186 # Get the biblio.record_entry.id value for the given identifier; note that this
187 # approach uses a wildcard to match anything that precedes the identifier value
188 sub map_id_to_bib {
189     my $record = shift;
190
191     my ($result, $evt);
192
193     my %search = (
194         tag => $bibfield, 
195         value => { like => '%' . $record }
196     );
197
198     if ($bibsubfield) {
199         $search{'subfield'} = $bibsubfield;
200     }
201
202     $result = $U->cstorereq(
203         'open-ils.cstore.direct.metabib.full_rec.search', \%search
204     );
205     $evt = OpenILS::Event->new('METABIB_FULL_REC_NOT_FOUND') unless $record;
206
207     return ($result, $evt);
208 }
209
210 __END__
211
212 =head1 NAME
213
214 marc2sre.pl - Convert MARC Format for Holdings Data (MFHD) records to SRE
215 (serial.record_entry) JSON objects 
216
217 =head1 SYNOPSIS
218
219 C<marc2sre.pl> [B<--config>=I<opensrf_core.conf>]
220 [[B<--idfield>=I<MARC-tag>[ B<--idsubfield>=I<MARC-code>]] [B<--start_id>=I<start-ID>]
221 [B<--user>=I<db-username>] [B<--marctype>=I<fileformat>]
222 [[B<--file>=I<MARC-filename>[, ...]] [B<--libmap>=I<map-file>] [B<--quiet>=I<quiet>]
223 [[B<--bibfield>=I<MARC-tag> [B<--bibsubfield>=<MARC-code>]]
224
225 =head1 DESCRIPTION
226
227 For one or more files containing MFHD records, iterate through the records
228 and generate SRE (serial.record_entry) JSON objects.
229
230 =head1 OPTIONS
231
232 =over
233
234 =item * B<-c> I<config-file>, B<--config>=I<config-file>
235
236 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
237 Defaults to F</openils/conf/opensrf_core.xml>
238
239 =item * B<--idfield> I<MARC-field>
240
241 Specifies the MFHD field where the identifier of the corresponding
242 bibliographic record is found. Defaults to '004'.
243
244 =item * B<--idsubfield> I<MARC-code>
245
246 Specifies the MFHD subfield, if any, where the identifier of the corresponding
247 bibliographic record is found. This option is ignored unless it is accompanied
248 by the B<--idfield> option.  Defaults to null.
249
250 =item * B<--bibfield> I<MARC-field>
251
252 Specifies the field in the bibliographic record that holds the identifier
253 value. Defaults to null.
254
255 =item * B<--bibsubfield> I<MARC-code>
256
257 Specifies the subfield in the bibliographic record, if any, that holds the
258 identifier value. This option is ignored unless it is accompanied by the
259 B<--bibfield> option. Defaults to null.
260
261 =item * B<-u> I<username>, B<--user>=I<username>
262
263 Specifies the Evergreen user that will own these serial records.
264
265 =item * B<-m> I<file-format>, B<--marctype>=I<file-format>
266
267 Specifies whether the files containg the MFHD records are in MARC21 ('MARC21')
268 or MARC21XML ('XML') format. Defaults to MARC21.
269
270 =item * B<-l> I<map-file>, B<--libmap>=I<map-file>
271
272 Points to a file to containing a mapping of library names to integers.
273 The integer represents the actor.org_unit.id value of the library. This enables
274 us to generate an ingest file that does not subsequently need to manually
275 manipulated.
276
277 The library name must correspond to the 'b' subfield of the 852 field.
278 Well, it does not have to, but you will have to modify this script
279 accordingly.
280
281 The format of the map file should be the name of the library, followed
282 by a tab, followed by the desired numeric ID of the library. For example:
283
284 BR1     4
285 BR2     5
286
287 =item * B<-q>, B<--quiet>
288
289 Suppresses the record counter output.
290
291 =back
292
293 =head1 EXAMPLES
294
295     marc2sre.pl --idfield 004 --bibfield 035 --bibsubfield a --user cat1 serial_holding.xml
296
297 Processes MFHD records in the B<serial_holding.xml> file. The script pulls the
298 bibliographic record identifier from the 004 control field of the MFHD record
299 and searches for a matching value in the bibliographic record in data field
300 035, subfield a. The "cat1" user will own the processed MFHD records.
301
302 =head1 AUTHOR
303
304 Dan Scott <dscott@laurentian.ca>
305
306 =head1 COPYRIGHT AND LICENSE
307
308 Copyright 2010-2011 by Dan Scott
309
310 This program is free software; you can redistribute it and/or
311 modify it under the terms of the GNU General Public License
312 as published by the Free Software Foundation; either version 2
313 of the License, or (at your option) any later version.
314
315 This program is distributed in the hope that it will be useful,
316 but WITHOUT ANY WARRANTY; without even the implied warranty of
317 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
318 GNU General Public License for more details.
319
320 You should have received a copy of the GNU General Public License
321 along with this program; if not, write to the Free Software
322 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
323
324 =cut