]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/support-scripts/marc_stream_importer.pl
instead of reading the MARC data directly from the socket, set the end-of-line charac...
[working/Evergreen.git] / Open-ILS / src / support-scripts / marc_stream_importer.pl
1 #!/usr/bin/perl
2 # Copyright (C) 2008-2010 Equinox Software, Inc.
3 # Author: Bill Erickson <erickson@esilibrary.com>
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14
15
16 use strict; use warnings;
17 use Net::Server::PreFork;
18 use base qw/Net::Server::PreFork/;
19 use MARC::Record;
20 use MARC::Batch;
21 use MARC::File::XML;
22 use MARC::File::USMARC;
23
24 use Data::Dumper;
25 use File::Basename qw/fileparse/;
26 use File::Temp;
27 use Getopt::Long qw(:DEFAULT GetOptionsFromArray);
28 use Pod::Usage;
29
30 use OpenSRF::Utils::Logger qw/$logger/;
31 use OpenSRF::AppSession;
32 use OpenSRF::EX qw/:try/;
33 use OpenILS::Utils::Cronscript;
34 require 'oils_header.pl';
35 use vars qw/$apputils/;
36
37 my $vl_ses;
38
39 my $debug = 0;
40
41 my %defaults = (
42     'buffsize=i'    => 4096,
43     'merge-profile=i' => 0,
44     'source=i'      => 1,
45 #    'osrf-config=s' => '/openils/conf/opensrf_core.xml',
46     'user=s'        => 'admin',
47     'password=s'    => '',
48     'tempdir=s'     => '',
49     'nolockfile'    => 1,
50     'queue=i'       => 1,
51     'noqueue'       => 0,
52     'wait=i'        => 5,
53     'import-by-queue' => 0
54 );
55
56 $OpenILS::Utils::Cronscript::debug=1 if $debug;
57 $Getopt::Long::debug=1 if $debug > 1;
58 my $o = OpenILS::Utils::Cronscript->new(\%defaults);
59
60 my @script_args = ();
61
62 if (grep {$_ eq '--'} @ARGV) {
63     print "Splitting options into groups\n" if $debug;
64     while (@ARGV) {
65         $_ = shift @ARGV;
66         $_ eq '--' and last;    # stop at the first --
67         push @script_args, $_;
68     }
69 } else {
70     @script_args = @ARGV;
71     @ARGV = ();
72 }
73
74 print "Calling MyGetOptions ",
75     (@script_args ? "with options: " . join(' ', @script_args) : 'without options from command line'),
76     "\n" if $debug;
77
78 my $real_opts = $o->MyGetOptions(\@script_args);
79 $o->bootstrap;
80 # GetOptionsFromArray(\@script_args, \%defaults, %defaults); # similar to
81
82 $real_opts->{tempdir} ||= tempdir_setting();    # This doesn't go in defaults because it reads config, must come after bootstrap
83
84 my $bufsize       = $real_opts->{buffsize};
85 my $bib_source    = $real_opts->{source};
86 my $osrf_config   = $real_opts->{'osrf-config'};
87 my $oils_username = $real_opts->{user};
88 my $oils_password = $real_opts->{password};
89 my $help          = $real_opts->{help};
90 my $merge_profile = $real_opts->{'merge-profile'};
91 my $queue_id      = $real_opts->{queue};
92 my $tempdir       = $real_opts->{tempdir};
93 my $import_by_queue  = $real_opts->{'import-by-queue'};
94    $debug        += $real_opts->{debug};
95
96 foreach (keys %$real_opts) {
97     print("real_opt->{$_} = ", $real_opts->{$_}, "\n") if $real_opts->{debug} or $debug;
98 }
99 my $wait_time     = $real_opts->{wait};
100 my $authtoken     = '';
101
102 # DEFAULTS for Net::Server
103 my $filename   = fileparse($0, '.pl');
104 my $conf_file  = (-r "$filename.conf") ? "$filename.conf" : undef;
105 # $conf_file is the Net::Server config for THIS script (not EG), if it exists and is readable
106
107
108 # FEEDBACK
109
110 pod2usage(1) if $help;
111 unless ($oils_password) {
112     print STDERR "\nERROR: password option required for session login\n\n";
113     # pod2usage(1);
114 }
115
116 print Dumper($o) if $debug;
117
118 if ($debug) {
119     foreach my $ref (qw/bufsize bib_source osrf_config oils_username oils_password help conf_file debug/) {
120         no strict 'refs';
121         printf "%16s => %s\n", $ref, (eval("\$$ref") || '');
122     }
123 }
124
125 print warning();
126 print Dumper($real_opts);
127
128 # SUBS
129
130 sub tempdir_setting {
131     my $ret = $apputils->simplereq( qw# opensrf.settings opensrf.settings.xpath.get
132         /opensrf/default/apps/open-ils.vandelay/app_settings/databases/importer # );
133     return $ret->[0] || '/tmp';
134 }
135
136 sub warning {
137     return <<WARNING;
138
139 WARNING:  This script provides no security layer.  Any client that has 
140 access to the server+port can inject MARC records into the system.  
141
142 WARNING
143 }
144
145 sub xml_import {
146     return $apputils->simplereq(
147         'open-ils.cat', 
148         'open-ils.cat.biblio.record.xml.import',
149         @_
150     );
151 }
152
153 sub old_process_batch_data {
154     my $data = shift or $logger->error("process_batch_data called without any data");
155     $data or return;
156
157     my $handle;
158     open $handle, '<', \$data; 
159     my $batch = MARC::Batch->new('USMARC', $handle);
160     $batch->strict_off;
161
162     my $index = 0;
163     while (1) {
164         my $rec;
165         $index++;
166
167         eval { $rec = $batch->next; };
168
169         if ($@) {
170             $logger->error("Failed parsing MARC record $index");
171             next;
172         }
173         last unless $rec;   # The only way out
174
175         my $resp = xml_import($authtoken, $rec->as_xml_record, $bib_source);
176
177         # has the session timed out?
178         if (oils_event_equals($resp, 'NO_SESSION')) {
179             new_auth_token();
180             $resp = xml_import($authtoken, $rec->as_xml_record, $bib_source);   # try again w/ new token
181         }
182         oils_event_die($resp);
183     }
184     return $index;
185 }
186
187 sub process_spool { # filename
188
189     my $marcfile = shift;
190     my @rec_ids;
191
192     if($import_by_queue) {
193
194         # don't collect the record IDs, just spool the queue
195
196         $apputils->simplereq(
197             'open-ils.vandelay', 
198             'open-ils.vandelay.bib.process_spool', 
199             $authtoken, 
200             undef, 
201             $queue_id, 
202             'import', 
203             $marcfile,
204             $bib_source 
205         );
206
207     } else {
208
209         # collect the newly queued record IDs for processing
210
211         my $req = $vl_ses->request(
212             'open-ils.vandelay.bib.process_spool.stream_results',
213             $authtoken, 
214             undef, # cache key not needed
215             $queue_id, 
216             'import', 
217             $marcfile, 
218             $bib_source 
219         );
220     
221         while(my $resp = $req->recv) {
222
223             if($req->failed) {
224                 $logger->error("Error spooling MARC data: $resp");
225
226             } elsif($resp->content) {
227                 push(@rec_ids, $resp->content);
228             }
229         }
230     }
231
232     return \@rec_ids;
233 }
234
235 sub bib_queue_import {
236     my $rec_ids = shift;
237     my $extra = {auto_overlay_exact => 1};
238     $extra->{merge_profile} = $merge_profile if $merge_profile;
239
240     my $req;
241     my @cleanup_recs;
242
243     if($import_by_queue) {
244         # import by queue
245
246         $req = $vl_ses->request(
247             'open-ils.vandelay.bib_queue.import', 
248             $authtoken, 
249             $queue_id, 
250             $extra 
251         );
252
253     } else {
254         # import explicit record IDs
255
256         $req = $vl_ses->request(
257             'open-ils.vandelay.bib_record.list.import', 
258             $authtoken, 
259             $rec_ids, 
260             $extra 
261         );
262     }
263
264     # collect the successfully imported vandelay records
265     while(my $resp = $req->recv) {
266          if($req->failed) {
267             $logger->error("Error importing MARC data: $resp");
268
269         } elsif(my $data = $resp->content) {
270             push(@cleanup_recs, $data->{imported}) unless $data->{err_event};
271         }
272     }
273
274     # clean up the successfully imported vandelay records to prevent queue bloat
275     my $pcrud = OpenSRF::AppSession->create('open-ils.pcrud');
276     $pcrud->connect;
277     $pcrud->request('open-ils.pcrud.transaction.begin', $authtoken)->recv;
278     my $err;
279
280     foreach (@cleanup_recs) {
281
282         try { 
283
284             $pcrud->request('open-ils.pcrud.delete.vqbr', $authtoken, $_)->recv;
285
286         } catch Error with {
287             $err = shift;
288             $logger->error("Error deleteing queued bib record $_: $err");
289         };
290     }
291
292     $pcrud->request('open-ils.pcrud.transaction.commit', $authtoken)->recv unless $err;
293     $pcrud->disconnect;
294 }
295
296 sub process_batch_data {
297     my $data = shift or $logger->error("process_batch_data called without any data");
298     $data or return;
299
300     $vl_ses = OpenSRF::AppSession->create('open-ils.vandelay');
301
302     my ($handle, $tempfile) = File::Temp->tempfile("$0_XXXX", DIR => $tempdir) or die "Cannot write tempfile in $tempdir";
303     print $handle $data;
304     close $handle;
305        
306     $logger->info("Calling process_spool on tempfile $tempfile (queue: $queue_id; source: $bib_source)");
307     my $rec_ids = process_spool($tempfile);
308
309     if (oils_event_equals($rec_ids, 'NO_SESSION')) {  # has the session timed out?
310         new_auth_token();
311         $rec_ids = process_spool($tempfile);                # try again w/ new token
312     }
313
314     my $resp = bib_queue_import($rec_ids);
315
316     if (oils_event_equals($resp, 'NO_SESSION')) {  # has the session timed out?
317         new_auth_token();
318         $resp = bib_queue_import();                # try again w/ new token
319     }
320     oils_event_die($resp);
321 }
322
323 sub process_request {   # The core Net::Server method
324     local $/ = "\x1D"; # MARC record separator
325     $logger->info("stream parser received contact");
326     my $data;
327     eval {
328         alarm $wait_time; # prevent accidental tie ups of backend processes
329         $data = <STDIN>;
330         alarm 0;
331     };
332     $logger->info("stream parser read " . length($data) . " bytes");
333     if ($real_opts->{noqueue}) {
334         old_process_batch_data($data);
335     } else {
336         process_batch_data($data);
337     }
338 }
339
340
341 # the authtoken will timeout after the configured inactivity period.
342 # When that happens, get a new one.
343 sub new_auth_token {
344     $authtoken = oils_login($oils_username, $oils_password, 'staff') 
345         or die "Unable to login to Evergreen as user $oils_username";
346     return $authtoken;
347 }
348
349 ##### MAIN ######
350
351 osrf_connect($osrf_config);
352 new_auth_token();
353 print "Calling Net::Server run ", (@ARGV ? "with command-line options: " . join(' ', @ARGV) : ''), "\n";
354 __PACKAGE__->run(conf_file => $conf_file);
355
356 __END__
357
358 =head1 NAME
359
360 marc_stream_importer.pl - Import MARC records via bare socket connection.
361
362 =head1 SYNOPSIS
363
364 ./marc_stream_importer.pl [common opts ...] [script opts ...] -- [Net::Server opts ...] &
365
366 This script uses the EG common options from B<Cronscript>.  See --help output for those.
367
368 Run C<perldoc marc_stream_importer.pl> for full documentation.
369
370 Note the extra C<--> to separate options for the script wrapper from options for the
371 underlying L<Net::Server> options.  
372
373 Note: this script has to be run in the same directory as B<oils_header.pl>.
374
375 Typical execution will include a trailing C<&> to run in the background.
376
377 =head1 DESCRIPTION
378
379 This script is a L<Net::Server::PreFork> instance for shoving records into Evergreen from a remote system.
380
381 =head1 OPTIONS
382
383 The only required option is --password
384
385  --password         =<eg_password>
386  --user             =<eg_username>  default: admin
387  --source           =<bib_source>   default: 1         Integer
388  --merge-profile    =<i>            default: 0
389  --tempdir          =</temp/dir/>   default: from L<opensrf.conf> <open-ils.vandelay/app_settings/databases/importer>
390  --source           =<i>            default: 1
391  --import-by-queue  =<i>            default: 0
392
393
394 =head2 Old style: --noqueue and associated options
395
396 To bypass vandelay queue processing and push directly into the database (as the old style)
397
398  --noqueue         default: OFF
399  --buffsize =<i>   default: 4096    Buffer size.  Only used by --noqueue
400  --wait     =<i>   default: 5       Seconds to read socket before processing.  Only used by --noqueue
401
402 =head2 Net::Server Options
403
404 By default, the script will use the Net::Server configuration file B<marc_stream_importer.conf>.  You can 
405 override this by passing a filepath with the --conf_file option.
406
407 Other Net::Server options include: --port=<port> --min_servers=<X> --max_servers=<Y> and --log_file=[path/to/file]
408
409 See L<Net::Server> for a complete list.
410
411 =head2 Configuration
412
413 =head3 OCLC Connexion
414
415 To use this script with OCLC Connexion, configure the client as follows:
416
417 Under Tools -> Options -> Export (tab)
418    Create -> Choose Connection -> OK -> Leave translation at "None" 
419        -> Create -> Create -> choose TCP/IP (internet) 
420        -> Enter hostname and Port, leave 'Use Telnet Protocol' checked 
421        -> Create/OK your way out of the dialogs
422    Record Characteristics (button) -> Choose 'UTF-8 Unicode' for the Character Set
423    
424
425 OCLC and Connexion are trademark/service marks of OCLC Online Computer Library Center, Inc.
426
427 =head1 CAVEATS
428
429 WARNING: This script provides no inherent security layer.  Any client that has 
430 access to the server+port can inject MARC records into the system.  
431 Use the available options (like allow/deny) in the Net::Server config file 
432 or via the command line to restrict access as necessary.
433
434 =head1 EXAMPLES
435
436 ./marc_stream_importer.pl  \
437     admin open-ils connexion --port 5555 --min_servers 2 \
438     --max_servers=20 --log_file=/openils/var/log/marc_net_importer.log &
439
440 =head1 SEE ALSO
441
442 L<Net::Server::PreFork>, L<marc_stream_importer.conf>
443
444 =head1 AUTHORS
445
446     Bill Erickson <erickson@esilibrary.com>
447     Joe Atzberger <jatzberger@esilibrary.com>
448
449 =cut