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