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