]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/marc_stream_importer.pl
LP#1807398 marc_stream/oils_header auth repairs
[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 $workstation = '';
63 my $tempdir     = '';
64 my $spoolfile   = '';
65 my $wait_time   = 5;
66 my $verbose     = 0;
67 my $bib_merge_profile;
68 my $auth_merge_profile;
69 my $bib_queue;
70 my $auth_queue;
71 my $bib_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     'workstation=s'         => \$workstation,
99     'tempdir=s'             => \$tempdir,
100     'spoolfile=s'           => \$spoolfile,
101     'wait=i'                => \$wait_time,
102     'merge-profile=i'       => \$bib_merge_profile,
103     'queue=i'               => \$deprecated_queue,
104     'bib-queue=i'           => \$bib_queue,
105     'source=i'              => \$bib_source,
106     'auth-merge-profile=i'  => \$auth_merge_profile,
107     'auth-queue=i'          => \$auth_queue,
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     --workstation
148         Evergreen workstation
149
150     --tempdir
151         MARC data received via the network is stored in a temporary
152         file so Vandelay can access it.  This must be a directory
153         the open-ils.vandelay service can access.  If you want the
154         file deleted after completion, be sure open-ils.vandelay
155         has write access to the directory and the file.
156         This value defaults to the Vandelay data directory, however
157         this configuratoin value is only accessible when run from 
158         the private opensrf domain, which you may not want to do.
159
160     --spoolfile
161         Path to a MARC file to load.  When a --spoolfile is specified,
162         this script will send the file to vandelay for processing,
163         then exit when complete.  In other words, it does not stay
164         alive to accept requests from the network.
165
166     --wait
167         Amount of time in seconds this script will wait after receiving
168         a connection on the socket and before recieving a complete
169         MARC record.  This prevents unintentional denial of service by 
170         clients connecting and never sending anything.
171
172     --merge-profile
173         ID of the vandelay bib record merge profile
174
175     --queue
176         ID of the vandelay bib record queue
177
178     --source
179         ID of the bib source
180
181     --auth-merge-profile
182         ID of the vandelay authority record merge profile
183
184     --auth-queue
185         ID of the vandelay authority record queue
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, --password, AND --workstation required.  --help for more info.\n" 
235     unless $username and $password and $workstation;
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' ?  '' : $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     my $api = 'open-ils.pcrud.delete.';
380     $api .= $cur_rec_type eq 'auth' ? 'vqar' : 'vqbr';
381
382     foreach (@cleanup_recs) {
383         eval {
384             $pcrud->request($api, $authtoken, $_)->recv;
385         };
386
387         if ($@) {
388             $logger->error("Error deleting queued $cur_rec_type record $_: $@");
389             last;
390         }
391     }
392
393     $pcrud->request('open-ils.pcrud.transaction.commit', $authtoken)->recv unless $err;
394     $pcrud->disconnect;
395
396     $logger->info("imported queued vandelay records: @cleanup_recs");
397     return (scalar(@cleanup_recs), $failed);
398 }
399
400
401
402 # Each child needs its own opensrf connection.
403 sub child_init_hook {
404     OpenSRF::System->bootstrap_client(config_file => $osrf_config);
405     Fieldmapper->import(IDL => 
406         OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
407 }
408
409
410 # The core Net::Server method
411 # Reads streams of MARC data from the network, saves the data as a file,
412 # then processes the file via vandelay.
413 sub process_request { 
414     my $self = shift;
415     my $client = $self->{server}->{peeraddr}.':'.$self->{server}->{peerport};
416
417     $logger->info("$client opened a new connection");
418
419     my $ph = OpenSRF::Transport::PeerHandle->retrieve;
420     if(!$ph->flush_socket()) {
421         $logger->error("We received a request, but we are no longer connected".
422             " to opensrf.  Exiting and dropping request from $client");
423         exit;
424     }
425
426     my $data = '';
427     eval {
428         local $SIG{ALRM} = sub { die "alarm\n" };
429         alarm $wait_time; # prevent accidental tie ups of backend processes
430         local $/ = "\x1D"; # MARC record separator
431         $data = <STDIN>;
432         alarm 0;
433     };
434
435     if($@) {
436         $logger->error("reading from STDIN failed or timed out: $@");
437         return;
438     } 
439
440     $logger->info("stream parser read " . length($data) . " bytes");
441
442     set_tempdir();
443
444     # copy data to a temporary file so vandelay can scoop it up
445     my ($handle, $tempfile) = tempfile("$0_XXXX", DIR => $tempdir) 
446         or die "Cannot create tempfile in $tempdir : $!";
447
448     print $handle $data or die "Error writing to tempfile $tempfile : $!\n";
449     close $handle;
450
451     process_file($tempfile);
452 }
453
454 sub set_merge_profile {
455
456     # serve from cache
457
458     return $cur_merge_profile = $bib_merge_profile_obj
459         if $bib_merge_profile_obj and $cur_rec_type eq 'bib';
460
461     return $cur_merge_profile = $auth_merge_profile_obj
462         if $auth_merge_profile_obj and $cur_rec_type eq 'auth';
463
464     # fetch un-cached profile
465     
466     my $profile_id = $cur_rec_type eq 'bib' ?
467         $bib_merge_profile : $auth_merge_profile;
468
469     return $cur_merge_profile = undef unless $profile_id;
470
471     $cur_merge_profile = $apputils->simplereq(
472         'open-ils.pcrud', 
473         'open-ils.pcrud.retrieve.vmp', 
474         $authtoken, $profile_id);
475
476     # cache profile for later
477    
478     $auth_merge_profile_obj = $cur_merge_profile if $cur_rec_type eq 'auth';
479     $bib_merge_profile_obj = $cur_merge_profile if $cur_rec_type eq 'bib';
480 }
481
482 sub process_file {
483     my $file = shift;
484
485     new_auth_token(); # login
486     my $rec_ids = process_spool($file);
487     my ($imported, $failed) = import_queued_records($rec_ids);
488
489     if (oils_event_equals($imported, 'NO_SESSION')) {  
490         # did the session expire while spooling?
491         new_auth_token(); # retry with new authtoken
492         ($imported, $failed) = import_queued_records($rec_ids);
493     }
494
495     oils_event_die($imported);
496
497     my $profile = $cur_merge_profile ? $cur_merge_profile->name : '';
498     my $msg = '';
499     $msg .= "Successfully imported $imported $cur_rec_type records ".
500         "using merge profile '$profile'\n" if $imported;
501     $msg .= "Failed to import $failed $cur_rec_type records\n" if $failed;
502     $msg .= "\x00" unless $spoolfile;
503     print $msg;
504
505     clear_auth_token(); # logout
506 }
507
508 # the authtoken will timeout after the configured inactivity period.
509 # When that happens, get a new one.
510 sub new_auth_token {
511     oils_login($username, $password, 'staff', $workstation)
512         or die "Unable to login to Evergreen as user $username";
513 }
514
515 sub clear_auth_token {
516     $apputils->simplereq(
517         'open-ils.auth',
518         'open-ils.auth.session.delete',
519         $authtoken
520     );
521     $authtoken = undef;
522 }
523
524 # -- execution starts here
525
526 if ($spoolfile) {
527     # individual files are processed in standalone mode.
528     # No Net::Server innards are necessary.
529
530     child_init_hook(); # force an opensrf connection
531     process_file($spoolfile);
532     exit;
533 }
534
535 # No spoolfile, run in Net::Server mode
536
537 warn <<WARNING;
538
539 WARNING:  This script provides no security layer.  Any client that has 
540 access to the server+port can inject MARC records into the system.  
541
542 WARNING
543
544 my %args;
545 $args{conf_file} = $net_server_conf if -r $net_server_conf;
546 $args{port} = $port if $port;
547
548 __PACKAGE__->run(%args);
549
550