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