]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/marc_stream_importer.pl.in
LP2061136 - Stamping 1405 DB upgrade script
[Evergreen.git] / Open-ILS / src / support-scripts / marc_stream_importer.pl.in
1 #!/usr/bin/perl
2 # Copyright (C) 2008-2014 Equinox Software, Inc.
3 # Copyright (C) 2014 King County Library System
4 # Author: Bill Erickson <berickxx@gmail.com>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # ---------------------------------------------------------------
17 # Sends MARC records, either from a file or from data delivered
18 # via the network, to open-ils.vandelay to be imported.
19 # ---------------------------------------------------------------
20 use strict; 
21 use warnings;
22 use Net::Server::PreFork;
23 use base qw/Net::Server::PreFork/;
24 use FindBin;
25 require "$FindBin::Bin/oils_header.pl";
26
27 use vars qw/$apputils $authtoken/;
28
29 use Getopt::Long;
30 use MARC::Record;
31 use MARC::Batch;
32 use MARC::File::XML (BinaryEncoding => 'UTF-8');
33 use MARC::File::USMARC;
34 use File::Basename qw/fileparse/;
35 use File::Temp qw/tempfile/;
36 use OpenSRF::AppSession;
37 use OpenSRF::Utils::Logger qw/$logger/;
38 use OpenSRF::Transport::PeerHandle;
39 use OpenSRF::Utils::SettingsClient;
40
41 use Data::Dumper;
42 $Data::Dumper::Indent=0; # for logging
43
44 # This script will always be an entry point for opensrf, 
45 # so go ahead and force log client.
46 $ENV{OSRF_LOG_CLIENT} = 1;
47
48 # these are updated with each new batch of records
49 my $cur_rec_type;
50 my $cur_rec_source;
51 my $cur_queue;
52
53 # cache these
54 my $cur_merge_profile; # this is an object
55 my $bib_merge_profile_obj;
56 my $auth_merge_profile_obj;
57
58 # options
59 my $help        = 0;
60 my $osrf_config = '@sysconfdir@/opensrf_core.xml';
61 my $username    = '';
62 my $password    = '';
63 my $workstation = '';
64 my $tempdir     = '';
65 my $spoolfile   = '';
66 my $wait_time   = 5;
67 my $verbose     = 0;
68 my $bib_merge_profile;
69 my $auth_merge_profile;
70 my $bib_queue;
71 my $auth_queue;
72 my $bib_source;
73 my $port;
74 my $bib_import_no_match;
75 my $bib_auto_overlay_exact;
76 my $bib_auto_overlay_1match;
77 my $bib_auto_overlay_best_match;
78 my $bib_match_quality_ratio;
79 my $auth_import_no_match;
80 my $auth_auto_overlay_exact;
81 my $auth_auto_overlay_1match;
82 my $auth_auto_overlay_best_match;
83 my $auth_match_quality_ratio;
84
85 # deprecated options;  these map to their bib_* equivalents
86 my $import_no_match;
87 my $auto_overlay_exact;
88 my $auto_overlay_1match;
89 my $auto_overlay_best_match;
90 my $deprecated_queue;
91
92
93
94 my $net_server_conf = (-r "@sysconfdir@/marc_stream_importer.conf") ? "@sysconfdir@/marc_stream_importer.conf" : undef;
95
96 GetOptions(
97     'osrf-config=s'         => \$osrf_config,
98     'verbose'               => \$verbose,
99     'username=s'            => \$username,
100     'password=s'            => \$password,
101     'workstation=s'         => \$workstation,
102     'tempdir=s'             => \$tempdir,
103     'spoolfile=s'           => \$spoolfile,
104     'wait=i'                => \$wait_time,
105     'merge-profile=i'       => \$bib_merge_profile,
106     'queue=i'               => \$deprecated_queue,
107     'bib-queue=i'           => \$bib_queue,
108     'source=i'              => \$bib_source,
109     'auth-merge-profile=i'  => \$auth_merge_profile,
110     'auth-queue=i'          => \$auth_queue,
111
112     # -- deprecated
113     'import-no-match'          => \$import_no_match,
114     'auto-overlay-exact'       => \$auto_overlay_exact,
115     'auto-overlay-1match'      => \$auto_overlay_1match,
116     'auto-overlay-best-match'  => \$auto_overlay_best_match,
117     # --
118
119     'bib-import-no-match'          => \$bib_import_no_match,
120     'bib-auto-overlay-exact'       => \$bib_auto_overlay_exact,
121     'bib-auto-overlay-1match'      => \$bib_auto_overlay_1match,
122     'bib-auto-overlay-best-match'  => \$bib_auto_overlay_best_match,
123     'bib-match-quality-ratio=f' => \$bib_match_quality_ratio,
124     'auth-import-no-match'         => \$auth_import_no_match,
125     'auth-auto-overlay-exact'      => \$auth_auto_overlay_exact,
126     'auth-auto-overlay-1match'     => \$auth_auto_overlay_1match,
127     'auth-auto-overlay-best-match' => \$auth_auto_overlay_best_match,
128     'auth-match-quality-ratio=f' => \$auth_match_quality_ratio,
129     'help'                  => \$help,
130     'net-server-config=s'   => \$net_server_conf,
131     'port=i'                => \$port
132 );
133
134 sub usage {
135     print <<USAGE;
136     --osrf-config
137         Path to OpenSRF configuration file. 
138
139     --net-server-conf
140         Path to Net::Server configuration file.  Defaults to $net_server_conf.
141         Only required if --spoolfile is not set.
142
143     --verbose               
144         Log additional details
145
146     --username
147         Evergreen user account which performs the import actions.
148
149     --password
150         Evergreen user account password
151
152     --workstation
153         Evergreen workstation
154
155     --tempdir
156         MARC data received via the network is stored in a temporary
157         file so Vandelay can access it.  This must be a directory
158         the open-ils.vandelay service can access.  If you want the
159         file deleted after completion, be sure open-ils.vandelay
160         has write access to the directory and the file.
161         This value defaults to the Vandelay data directory, however
162         this configuratoin value is only accessible when run from 
163         the private opensrf domain, which you may not want to do.
164
165     --spoolfile
166         Path to a MARC file to load.  When a --spoolfile is specified,
167         this script will send the file to vandelay for processing,
168         then exit when complete.  In other words, it does not stay
169         alive to accept requests from the network.
170
171     --wait
172         Amount of time in seconds this script will wait after receiving
173         a connection on the socket and before recieving a complete
174         MARC record.  This prevents unintentional denial of service by 
175         clients connecting and never sending anything.
176
177     --merge-profile
178         ID of the vandelay bib record merge profile
179
180     --queue
181         ID of the vandelay bib record queue
182
183     --source
184         ID of the bib source
185
186     --auth-merge-profile
187         ID of the vandelay authority record merge profile
188
189     --auth-queue
190         ID of the vandelay authority record queue
191
192     --bib-import-no-match
193     --bib-auto-overlay-exact
194     --bib-auto-overlay-1match
195     --bib-auto-overlay-best-match
196     --bib-match-quality-ratio
197     --auth-import-no-match
198     --auth-auto-overlay-exact
199     --auth-auto-overlay-1match
200     --auth-auto-overlay-best-match
201     --auth-match-quality-ratio
202
203         Bib and auth import options which map directly to Vandelay import 
204         options.  
205
206         For example: 
207             Apply import-no-match to bibs and auto-overlay-exact to auths.
208
209             $0 --bib-import-no-match --auth-auto-overlay-exact
210
211     --help                  
212         Show this help message
213 USAGE
214     exit;
215 }
216
217 usage() if $help;
218
219 if ($import_no_match) {
220     warn "\nimport-no-match is deprecated; use bib-import-no-match\n";
221     $bib_import_no_match = $import_no_match;
222 }
223 if ($auto_overlay_exact) {
224     warn "\nauto-overlay-exact is deprecated; use bib-auto-overlay-exact\n";
225     $bib_auto_overlay_exact = $auto_overlay_exact;
226 }
227 if ($auto_overlay_1match) {
228     warn "\nauto-overlay-1match is deprecated; use bib-auto-overlay-1match\n";
229     $bib_auto_overlay_1match = $auto_overlay_1match;
230 }
231 if ($auto_overlay_best_match) {
232     warn "\nauto-overlay-best-match is deprecated; use bib-auto-overlay-best-match\n";
233     $bib_auto_overlay_best_match = $auto_overlay_best_match;
234 }
235 if ($deprecated_queue) {
236     warn "\n--queue is deprecated; use --bib-queue\n";
237     $bib_queue = $deprecated_queue;
238 }
239
240
241 die "--username, --password, AND --workstation required.  --help for more info.\n" 
242     unless $username and $password and $workstation;
243 die "--bib-queue OR --auth-queue required.  --help for more info.\n" 
244     unless $bib_queue or $auth_queue;
245
246 sub set_tempdir {
247     return if $tempdir; # already read or user provided
248     $tempdir = OpenSRF::Utils::SettingsClient->new->config_value(
249         qw/apps open-ils.vandelay app_settings databases importer/
250     ) || '/tmp';
251 }
252
253 # Sets cur_rec_type to 'auth' if leader/06 of the first 
254 # parseable record is 'z', otherwise 'bib'.
255 sub set_record_type {
256     my $file_name = shift;
257
258     my $marctype = 'USMARC';
259     open(F, $file_name) or
260         die "Unable to open MARC file $file_name : $!\n";
261     $marctype = 'XML' if (getc(F) =~ /^\D/o);
262     close F;
263
264     my $batch = new MARC::Batch ($marctype, $file_name);
265     $batch->strict_off;
266
267     my $rec;
268     my $ldr_06 = '';
269     while (1) {
270         eval {$rec = $batch->next};
271         next if $@; # record parse failure
272         last unless $rec;
273         $ldr_06 = substr($rec->leader(), 6, 1) || '';
274         last;
275     }
276
277     $cur_rec_type = $ldr_06 eq 'z' ? 'auth' : 'bib';
278
279     $cur_queue = $cur_rec_type eq 'auth' ? $auth_queue : $bib_queue;
280     $cur_rec_source = $cur_rec_type eq 'auth' ?  '' : $bib_source;
281     set_merge_profile();
282 }
283
284 # set vandelay options based on command line ops and the type of record
285 # currently in process.
286 sub compile_vandelay_ops {
287
288     my $vl_ops = {
289         report_all => 1,
290         merge_profile => $cur_merge_profile ? $cur_merge_profile->id : undef
291     };
292
293     if ($cur_rec_type eq 'auth') {
294         $vl_ops->{import_no_match} = $auth_import_no_match;
295         $vl_ops->{auto_overlay_exact} = $auth_auto_overlay_exact;
296         $vl_ops->{auto_overlay_1match} = $auth_auto_overlay_1match;
297         $vl_ops->{auto_overlay_best_match} = $auth_auto_overlay_best_match;
298         $vl_ops->{match_quality_ratio} = $auth_match_quality_ratio;
299     } else {
300         $vl_ops->{import_no_match} = $bib_import_no_match;
301         $vl_ops->{auto_overlay_exact} = $bib_auto_overlay_exact;
302         $vl_ops->{auto_overlay_1match} = $bib_auto_overlay_1match;
303         $vl_ops->{auto_overlay_best_match} = $bib_auto_overlay_best_match;
304         $vl_ops->{match_quality_ratio} = $bib_match_quality_ratio;
305     }
306
307     # Default to exact match only if not other strategy is selected.
308     $vl_ops->{auto_overlay_exact} = 1
309         if not (
310             $vl_ops->{auto_overlay_1match} or 
311             $vl_ops->{auto_overlay_best_match}
312         );
313
314     $logger->info("VL options: ".Dumper($vl_ops)) if $verbose;
315     return $vl_ops;
316 }
317
318 sub process_spool { 
319     my $file_name = shift; # filename
320
321     set_record_type($file_name);
322
323     my $ses = OpenSRF::AppSession->create('open-ils.vandelay');
324     my $req = $ses->request(
325         "open-ils.vandelay.$cur_rec_type.process_spool.stream_results",
326         $authtoken, undef, # cache key not needed
327         $cur_queue, 'import', $file_name, $cur_rec_source 
328     );
329
330     my @rec_ids;
331     while(my $resp = $req->recv) {
332
333         if($req->failed) {
334             $logger->error("Error spooling MARC data: $resp");
335
336         } elsif($resp->content) {
337             push(@rec_ids, $resp->content);
338         }
339     }
340
341     return \@rec_ids;
342 }
343
344 sub import_queued_records {
345     my $rec_ids = shift;
346     my $vl_ops = compile_vandelay_ops();
347
348     my $ses = OpenSRF::AppSession->create('open-ils.vandelay');
349     my $req = $ses->request(
350         "open-ils.vandelay.${cur_rec_type}_record.list.import",
351         $authtoken, $rec_ids, $vl_ops 
352     );
353
354     # collect the successfully imported vandelay records
355     my $failed = 0;
356     my @cleanup_recs;
357     while(my $resp = $req->recv) {
358          if($req->failed) {
359             $logger->error("Error importing MARC data: $resp");
360
361         } elsif(my $data = $resp->content) {
362
363             if($data->{err_event}) {
364
365                 $logger->error(Dumper($data->{err_event}));
366                 $failed++;
367
368             } elsif ($data->{no_import}) {
369                 # no errors, just didn't import, because of rules.
370
371                 $failed++;
372                 $logger->info(
373                     "record failed to satisfy Vandelay merge/quality/etc. ".
374                     "requirements: " . ($data->{imported} || ''));
375
376             } else {
377                 push(@cleanup_recs, $data->{imported}) if $data->{imported};
378             }
379         }
380     }
381
382     # clean up the successfully imported vandelay records to prevent queue bloat
383     my $pcrud = OpenSRF::AppSession->create('open-ils.pcrud');
384     $pcrud->connect;
385     $pcrud->request('open-ils.pcrud.transaction.begin', $authtoken)->recv;
386     my $err;
387
388     my $api = 'open-ils.pcrud.delete.';
389     $api .= $cur_rec_type eq 'auth' ? 'vqar' : 'vqbr';
390
391     foreach (@cleanup_recs) {
392         eval {
393             $pcrud->request($api, $authtoken, $_)->recv;
394         };
395
396         if ($@) {
397             $logger->error("Error deleting queued $cur_rec_type record $_: $@");
398             last;
399         }
400     }
401
402     $pcrud->request('open-ils.pcrud.transaction.commit', $authtoken)->recv unless $err;
403     $pcrud->disconnect;
404
405     $logger->info("imported queued vandelay records: @cleanup_recs");
406     return (scalar(@cleanup_recs), $failed);
407 }
408
409
410
411 # Each child needs its own opensrf connection.
412 sub child_init_hook {
413     OpenSRF::System->bootstrap_client(config_file => $osrf_config);
414     Fieldmapper->import(IDL => 
415         OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
416 }
417
418
419 # The core Net::Server method
420 # Reads streams of MARC data from the network, saves the data as a file,
421 # then processes the file via vandelay.
422 sub process_request { 
423     my $self = shift;
424     my $client = $self->{server}->{peeraddr}.':'.$self->{server}->{peerport};
425
426     $logger->info("$client opened a new connection");
427
428     my $ph = OpenSRF::Transport::PeerHandle->retrieve;
429     if(!$ph->flush_socket()) {
430         $logger->error("We received a request, but we are no longer connected".
431             " to opensrf.  Exiting and dropping request from $client");
432         exit;
433     }
434
435     my $data = '';
436     eval {
437         local $SIG{ALRM} = sub { die "alarm\n" };
438         alarm $wait_time; # prevent accidental tie ups of backend processes
439         local $/ = "\x1D"; # MARC record separator
440         $data = <STDIN>;
441         alarm 0;
442     };
443
444     if($@) {
445         $logger->error("reading from STDIN failed or timed out: $@");
446         return;
447     } 
448
449     $logger->info("stream parser read " . length($data) . " bytes");
450
451     set_tempdir();
452
453     # copy data to a temporary file so vandelay can scoop it up
454     my $base = fileparse($0, qw(.pl));
455     my ($handle, $tempfile) = tempfile("${base}_XXXX", DIR => $tempdir)
456         or die "Cannot create tempfile in $tempdir : $!";
457
458     print $handle $data or die "Error writing to tempfile $tempfile : $!\n";
459     close $handle;
460
461     process_file($tempfile);
462 }
463
464 sub set_merge_profile {
465
466     # serve from cache
467
468     return $cur_merge_profile = $bib_merge_profile_obj
469         if $bib_merge_profile_obj and $cur_rec_type eq 'bib';
470
471     return $cur_merge_profile = $auth_merge_profile_obj
472         if $auth_merge_profile_obj and $cur_rec_type eq 'auth';
473
474     # fetch un-cached profile
475     
476     my $profile_id = $cur_rec_type eq 'bib' ?
477         $bib_merge_profile : $auth_merge_profile;
478
479     return $cur_merge_profile = undef unless $profile_id;
480
481     $cur_merge_profile = $apputils->simplereq(
482         'open-ils.pcrud', 
483         'open-ils.pcrud.retrieve.vmp', 
484         $authtoken, $profile_id);
485
486     # cache profile for later
487    
488     $auth_merge_profile_obj = $cur_merge_profile if $cur_rec_type eq 'auth';
489     $bib_merge_profile_obj = $cur_merge_profile if $cur_rec_type eq 'bib';
490 }
491
492 sub process_file {
493     my $file = shift;
494
495     new_auth_token(); # login
496     my $rec_ids = process_spool($file);
497     my ($imported, $failed) = import_queued_records($rec_ids);
498
499     if (oils_event_equals($imported, 'NO_SESSION')) {  
500         # did the session expire while spooling?
501         new_auth_token(); # retry with new authtoken
502         ($imported, $failed) = import_queued_records($rec_ids);
503     }
504
505     oils_event_die($imported);
506
507     my $profile = $cur_merge_profile ? $cur_merge_profile->name : '';
508     my $msg = '';
509     $msg .= "Successfully imported $imported $cur_rec_type records ".
510         "using merge profile '$profile'\n" if $imported;
511     $msg .= "Failed to import $failed $cur_rec_type records\n" if $failed;
512     $msg .= "\x00" unless $spoolfile;
513     print $msg;
514
515     clear_auth_token(); # logout
516 }
517
518 # the authtoken will timeout after the configured inactivity period.
519 # When that happens, get a new one.
520 sub new_auth_token {
521     oils_login($username, $password, 'staff', $workstation)
522         or die "Unable to login to Evergreen as user $username";
523 }
524
525 sub clear_auth_token {
526     $apputils->simplereq(
527         'open-ils.auth',
528         'open-ils.auth.session.delete',
529         $authtoken
530     );
531     $authtoken = undef;
532 }
533
534 # -- execution starts here
535
536 if ($spoolfile) {
537     # individual files are processed in standalone mode.
538     # No Net::Server innards are necessary.
539
540     child_init_hook(); # force an opensrf connection
541     process_file($spoolfile);
542     exit;
543 }
544
545 # No spoolfile, run in Net::Server mode
546
547 warn <<WARNING;
548
549 WARNING:  This script provides no security layer.  Any client that has 
550 access to the server+port can inject MARC records into the system.  
551
552 WARNING
553
554 my %args;
555 $args{conf_file} = $net_server_conf if -r $net_server_conf;
556 $args{port} = $port if $port;
557
558 __PACKAGE__->run(%args);
559
560