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