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