install command-line MARC import tools in @prefix@/bin
[working/Evergreen.git] / Open-ILS / src / extras / import / marc2sre.pl.in
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 = '@sysconfdir@/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     if ($record_field) {
93         $record = $record_field->data;
94     }
95
96     # If we have been given bibfield / bibsubfield values, use those to find
97     # a matching bib record for $record and use _that_ as our record instead
98     if ($bibfield) {
99         my ($result, $evt) = map_id_to_bib($record);
100         if ($evt || !$result->record) {
101             print("Could not find matching bibliographic record for $record\n");
102         }
103         $record = $result->record;
104     } else {
105         # Strip the identifier down to a usable integer
106         $record =~ s/^.*?(\d+).*?$/$1/o;
107     }
108
109     (my $xml = $rec->as_xml_record()) =~ s/\n//sog;
110     $xml =~ s/^<\?xml.+\?\s*>//go;
111     $xml =~ s/>\s+</></go;
112     $xml =~ s/\p{Cc}//go;
113     $xml = OpenILS::Application::AppUtils->entityize($xml);
114     $xml =~ s/[\x00-\x1f]//go;
115
116     my $bib = new Fieldmapper::serial::record_entry;
117     $bib->id($id);
118     $bib->record($record);
119     $bib->active('t');
120     $bib->deleted('f');
121     $bib->marc($xml);
122     $bib->creator($user);
123     $bib->create_date('now');
124     $bib->editor($user);
125     $bib->edit_date('now');
126     $bib->last_xact_id('IMPORT-'.$starttime);
127
128     if ($libmap) {
129         my $lib_id = get_library_id($rec);
130         if ($lib_id) {
131             $bib->owning_lib($lib_id);
132         }
133     }
134
135     print OpenSRF::Utils::JSON->perl2JSON($bib)."\n";
136
137     $count++;
138
139     if (!$quiet && !($count % 20)) {
140         print STDERR "\r$count\t". $count / (time - $starttime);
141     }
142 }
143
144 # Generate a hash of library names (as found in the 852b in the MFHD record) to
145 # integers representing actor.org_unit ID values
146 sub map_libraries_to_ID {
147     my $map_filename = shift;
148
149     my %lib_id_map;
150
151     open(MAP_FH, '<', $map_filename) or die "Could not load [$map_filename] $!";
152     while (<MAP_FH>) {
153         my ($lib, $id) = $_ =~ /^(.*?)\t(.*?)$/;
154         $lib_id_map{$lib} = $id;
155     }
156
157     return \%lib_id_map;
158 }
159
160 # Look up the actor.org_unit.id value for this library name
161 sub get_library_id {
162     my $record = shift;
163
164     my $lib_name = $record->field('852')->subfield('b');
165     my $lib_id = $lib_id_map->{$lib_name};
166
167     return $lib_id;
168 }
169
170 # Get the actor.usr.id value for the given username
171 sub get_user_id {
172     my $username = shift;
173
174     my ($result, $evt);
175
176     $result = $U->cstorereq(
177         'open-ils.cstore.direct.actor.user.search',
178         { usrname => $username, deleted => 'f' }
179     );
180     $evt = OpenILS::Event->new('ACTOR_USR_NOT_FOUND') unless $result;
181
182     return ($result, $evt);
183 }
184
185 # Get the biblio.record_entry.id value for the given identifier; note that this
186 # approach uses a wildcard to match anything that precedes the identifier value
187 sub map_id_to_bib {
188     my $record = shift;
189
190     my ($result, $evt);
191
192     my %search = (
193         tag => $bibfield, 
194         value => { ilike => '%' . $record }
195     );
196
197     if ($bibsubfield) {
198         $search{'subfield'} = $bibsubfield;
199     }
200
201     $result = $U->cstorereq(
202         'open-ils.cstore.direct.metabib.full_rec.search', \%search
203     );
204     $evt = OpenILS::Event->new('METABIB_FULL_REC_NOT_FOUND') unless $record;
205
206     return ($result, $evt);
207 }
208
209 __END__
210
211 =head1 NAME
212
213 marc2sre.pl - Convert MARC Format for Holdings Data (MFHD) records to SRE
214 (serial.record_entry) JSON objects 
215
216 =head1 SYNOPSIS
217
218 C<marc2sre.pl> [B<--config>=I<opensrf_core.conf>]
219 [[B<--idfield>=I<MARC-tag>[ B<--idsubfield>=I<MARC-code>]] [B<--start_id>=I<start-ID>]
220 [B<--user>=I<db-username>] [B<--marctype>=I<fileformat>]
221 [[B<--file>=I<MARC-filename>[, ...]] [B<--libmap>=I<map-file>] [B<--quiet>=I<quiet>]
222 [[B<--bibfield>=I<MARC-tag> [B<--bibsubfield>=<MARC-code>]]
223
224 =head1 DESCRIPTION
225
226 For one or more files containing MFHD records, iterate through the records
227 and generate SRE (serial.record_entry) JSON objects.
228
229 =head1 OPTIONS
230
231 =over
232
233 =item * B<-c> I<config-file>, B<--config>=I<config-file>
234
235 Specifies the OpenSRF configuration file used to connect to the OpenSRF router.
236 Defaults to F<@sysconfdir@/opensrf_core.xml>
237
238 =item * B<--idfield> I<MARC-field>
239
240 Specifies the MFHD field where the identifier of the corresponding
241 bibliographic record is found. Defaults to '004'.
242
243 =item * B<--idsubfield> I<MARC-code>
244
245 Specifies the MFHD subfield, if any, where the identifier of the corresponding
246 bibliographic record is found. This option is ignored unless it is accompanied
247 by the B<--idfield> option.  Defaults to null.
248
249 =item * B<--bibfield> I<MARC-field>
250
251 Specifies the field in the bibliographic record that holds the identifier
252 value. Defaults to null.
253
254 =item * B<--bibsubfield> I<MARC-code>
255
256 Specifies the subfield in the bibliographic record, if any, that holds the
257 identifier value. This option is ignored unless it is accompanied by the
258 B<--bibfield> option. Defaults to null.
259
260 =item * B<-u> I<username>, B<--user>=I<username>
261
262 Specifies the Evergreen user that will own these serial records.
263
264 =item * B<-m> I<file-format>, B<--marctype>=I<file-format>
265
266 Specifies whether the files containg the MFHD records are in MARC21 ('MARC21')
267 or MARC21XML ('XML') format. Defaults to MARC21.
268
269 =item * B<-l> I<map-file>, B<--libmap>=I<map-file>
270
271 Points to a file to containing a mapping of library names to integers.
272 The integer represents the actor.org_unit.id value of the library. This enables
273 us to generate an ingest file that does not subsequently need to manually
274 manipulated.
275
276 The library name must correspond to the 'b' subfield of the 852 field.
277 Well, it does not have to, but you will have to modify this script
278 accordingly.
279
280 The format of the map file should be the name of the library, followed
281 by a tab, followed by the desired numeric ID of the library. For example:
282
283 BR1     4
284 BR2     5
285
286 =item * B<-q>, B<--quiet>
287
288 Suppresses the record counter output.
289
290 =back
291
292 =head1 EXAMPLES
293
294     marc2sre.pl --idfield 004 --bibfield 035 --bibsubfield a --user cat1 serial_holding.xml
295
296 Processes MFHD records in the B<serial_holding.xml> file. The script pulls the
297 bibliographic record identifier from the 004 control field of the MFHD record
298 and searches for a matching value in the bibliographic record in data field
299 035, subfield a. The "cat1" user will own the processed MFHD records.
300
301 =head1 AUTHOR
302
303 Dan Scott <dscott@laurentian.ca>
304
305 =head1 COPYRIGHT AND LICENSE
306
307 Copyright 2010-2011 by Dan Scott
308
309 This program is free software; you can redistribute it and/or
310 modify it under the terms of the GNU General Public License
311 as published by the Free Software Foundation; either version 2
312 of the License, or (at your option) any later version.
313
314 This program is distributed in the hope that it will be useful,
315 but WITHOUT ANY WARRANTY; without even the implied warranty of
316 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
317 GNU General Public License for more details.
318
319 You should have received a copy of the GNU General Public License
320 along with this program; if not, write to the Free Software
321 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
322
323 =cut