]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/support-scripts/marc_stream_importer.pl
LP1402770_column_picker_option_for_number_of_holds
[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     foreach (@cleanup_recs) {
375         eval {
376             $pcrud->request(
377                 'open-ils.pcrud.delete.vqbr', $authtoken, $_)->recv;
378         };
379
380         if ($@) {
381             $logger->error("Error deleteing queued bib record $_: $@");
382             last;
383         }
384     }
385
386     $pcrud->request('open-ils.pcrud.transaction.commit', $authtoken)->recv unless $err;
387     $pcrud->disconnect;
388
389     $logger->info("imported queued vandelay records: @cleanup_recs");
390     return (scalar(@cleanup_recs), $failed);
391 }
392
393
394
395 # Each child needs its own opensrf connection.
396 sub child_init_hook {
397     OpenSRF::System->bootstrap_client(config_file => $osrf_config);
398     Fieldmapper->import(IDL => 
399         OpenSRF::Utils::SettingsClient->new->config_value("IDL"));
400 }
401
402
403 # The core Net::Server method
404 # Reads streams of MARC data from the network, saves the data as a file,
405 # then processes the file via vandelay.
406 sub process_request { 
407     my $self = shift;
408     my $client = $self->{server}->{peeraddr}.':'.$self->{server}->{peerport};
409
410     $logger->info("$client opened a new connection");
411
412     my $ph = OpenSRF::Transport::PeerHandle->retrieve;
413     if(!$ph->flush_socket()) {
414         $logger->error("We received a request, but we are no longer connected".
415             " to opensrf.  Exiting and dropping request from $client");
416         exit;
417     }
418
419     my $data = '';
420     eval {
421         local $SIG{ALRM} = sub { die "alarm\n" };
422         alarm $wait_time; # prevent accidental tie ups of backend processes
423         local $/ = "\x1D"; # MARC record separator
424         $data = <STDIN>;
425         alarm 0;
426     };
427
428     if($@) {
429         $logger->error("reading from STDIN failed or timed out: $@");
430         return;
431     } 
432
433     $logger->info("stream parser read " . length($data) . " bytes");
434
435     set_tempdir();
436
437     # copy data to a temporary file so vandelay can scoop it up
438     my ($handle, $tempfile) = tempfile("$0_XXXX", DIR => $tempdir) 
439         or die "Cannot create tempfile in $tempdir : $!";
440
441     print $handle $data or die "Error writing to tempfile $tempfile : $!\n";
442     close $handle;
443
444     process_file($tempfile);
445 }
446
447 sub set_merge_profile {
448
449     # serve from cache
450
451     return $cur_merge_profile = $bib_merge_profile_obj
452         if $bib_merge_profile_obj and $cur_rec_type eq 'bib';
453
454     return $cur_merge_profile = $auth_merge_profile_obj
455         if $auth_merge_profile_obj and $cur_rec_type eq 'auth';
456
457     # fetch un-cached profile
458     
459     my $profile_id = $cur_rec_type eq 'bib' ?
460         $bib_merge_profile : $auth_merge_profile;
461
462     return $cur_merge_profile = undef unless $profile_id;
463
464     $cur_merge_profile = $apputils->simplereq(
465         'open-ils.pcrud', 
466         'open-ils.pcrud.retrieve.vmp', 
467         $authtoken, $profile_id);
468
469     # cache profile for later
470    
471     $auth_merge_profile_obj = $cur_merge_profile if $cur_rec_type eq 'auth';
472     $bib_merge_profile_obj = $cur_merge_profile if $cur_rec_type eq 'bib';
473 }
474
475 sub process_file {
476     my $file = shift;
477
478     new_auth_token(); # login
479     my $rec_ids = process_spool($file);
480     my ($imported, $failed) = import_queued_records($rec_ids);
481
482     if (oils_event_equals($imported, 'NO_SESSION')) {  
483         # did the session expire while spooling?
484         new_auth_token(); # retry with new authtoken
485         ($imported, $failed) = import_queued_records($rec_ids);
486     }
487
488     oils_event_die($imported);
489
490     my $profile = $cur_merge_profile ? $cur_merge_profile->name : '';
491     my $msg = '';
492     $msg .= "Successfully imported $imported $cur_rec_type records ".
493         "using merge profile '$profile'\n" if $imported;
494     $msg .= "Failed to import $failed $cur_rec_type records\n" if $failed;
495     $msg .= "\x00";
496     print $msg;
497
498     clear_auth_token(); # logout
499 }
500
501 # the authtoken will timeout after the configured inactivity period.
502 # When that happens, get a new one.
503 sub new_auth_token {
504     oils_login($username, $password, 'staff') 
505         or die "Unable to login to Evergreen as user $username";
506 }
507
508 sub clear_auth_token {
509     $apputils->simplereq(
510         'open-ils.auth',
511         'open-ils.auth.session.delete',
512         $authtoken
513     );
514     $authtoken = undef;
515 }
516
517 # -- execution starts here
518
519 if ($spoolfile) {
520     # individual files are processed in standalone mode.
521     # No Net::Server innards are necessary.
522
523     child_init_hook(); # force an opensrf connection
524     process_file($spoolfile);
525     exit;
526 }
527
528 # No spoolfile, run in Net::Server mode
529
530 warn <<WARNING;
531
532 WARNING:  This script provides no security layer.  Any client that has 
533 access to the server+port can inject MARC records into the system.  
534
535 WARNING
536
537 my %args;
538 $args{conf_file} = $net_server_conf if -r $net_server_conf;
539 $args{port} = $port if $port;
540
541 __PACKAGE__->run(%args);
542
543