add marc_export to list of scripts installed in /openils/bin
[Evergreen.git] / Open-ILS / src / support-scripts / marc_export.in
1 #!/usr/bin/perl
2 # vim:et:sw=4:ts=4:
3 use strict;
4 use warnings;
5 use bytes;
6
7 use OpenSRF::System;
8 use OpenSRF::EX qw/:try/;
9 use OpenSRF::AppSession;
10 use OpenSRF::Utils::JSON;
11 use OpenSRF::Utils::SettingsClient;
12 use OpenILS::Application::AppUtils;
13 use OpenILS::Utils::Fieldmapper;
14 use OpenILS::Utils::CStoreEditor;
15
16 use MARC::Record;
17 use MARC::File::XML;
18 use UNIVERSAL::require;
19
20 use Time::HiRes qw/time/;
21 use Getopt::Long;
22
23
24 my @formats = qw/USMARC UNIMARC XML BRE ARE/;
25
26 my $config = '@sysconfdir@/opensrf_core.xml';
27 my $format = 'USMARC';
28 my $encoding = 'MARC8';
29 my $location = '';
30 my $dollarsign = '$';
31 my $idl = 0;
32 my $help = undef;
33 my $holdings = undef;
34 my $timeout = 0;
35 my $export_mfhd = undef;
36 my $type = 'biblio';
37 my $all_records = undef;
38 my $replace_001 = undef;
39 my @library = ();
40
41 GetOptions(
42         'help'       => \$help,
43         'items'      => \$holdings,
44         'mfhd'       => \$export_mfhd,
45         'all'        => \$all_records,
46         'replace_001'=> \$replace_001,
47         'location=s' => \$location,
48         'money=s'    => \$dollarsign,
49         'config=s'   => \$config,
50         'format=s'   => \$format,
51         'type=s'     => \$type,
52         'xml-idl=s'  => \$idl,
53         'encoding=s' => \$encoding,
54         'timeout=i'  => \$timeout,
55         'library=s'  => \@library,
56 );
57
58 if ($help) {
59 print <<"HELP";
60 This script exports MARC authority, bibliographic, and serial holdings
61 records from an Evergreen database. 
62
63 Input to this script can consist of a list of record IDs, with one record ID
64 per line, corresponding to the record ID in the Evergreen database table of
65 your requested record type.
66
67 Alternately, passing the --all option will attempt to export all records of
68 the specified type from the Evergreen database. The --all option starts at
69 record ID 1 and increments the ID by 1 until the largest ID in the database
70 is retrieved. This may not be very efficient for databases with large gaps
71 in their ID sequences.
72
73 Usage: $0 [options]
74  --help or -h       This screen.
75  --config or -c     Configuration file [@sysconfdir@/opensrf_core.xml]
76  --format or -f     Output format (USMARC, UNIMARC, XML, BRE, ARE) [USMARC]
77  --encoding or -e   Output encoding (UTF-8, ISO-8859-?, MARC8) [MARC8]
78  --xml-idl or -x    Location of the IDL XML
79  --timeout          Timeout for exporting a single record; increase if you
80                     are using --holdings and are exporting records that
81                     have a lot of items attached to them.
82  --type or -t       Record type (BIBLIO, AUTHORITY) [BIBLIO]
83  --all or -a        Export all records; ignores input list
84  --library          Export the bibliographic records that have attached
85                     holdings for the listed library or libraries as
86                     identified by shortname
87  --replace_001      Replace the 001 field value with the record ID
88
89  Additional options for type = 'BIBLIO':
90  --items or -i      Include items (holdings) in the output
91  --money            Currency symbol to use in item price field [\$]
92  --mfhd             Export serial MFHD records for associated bib records
93                     Not compatible with --format=BRE
94  --location or -l   MARC Location Code for holdings from
95                     http://www.loc.gov/marc/organizations/orgshome.html
96
97 Examples:
98
99 To export a set of USMARC records in a file named "output_file" based on the
100 IDs contained in a file named "list_of_ids":
101   cat list_of_ids | $0 > output_file
102
103 To export a set of MARC21XML authority records in a file named "output.xml"
104 for all authority records in the database:
105   $0 --format XML --type AUTHORITY --all > output.xml
106
107 To export a set of USMARC bibliographic records encoded in UTF-8 in a file
108 named "sys1_bibs.mrc" based on records which have attached callnumbers for the
109 libraries with the short names "BR1" and "BR2":
110
111   $0 --library BR1 --library BR2 --encoding UTF-8 > sys1_bibs.mrc
112
113 HELP
114     exit;
115 }
116
117 if ($all_records && @library) {
118     die('Incompatible arguments: you cannot combine a request for all ' .
119         'records with a request for records by library');
120 }
121
122 $type = lc($type);
123 $format = uc($format);
124 $encoding = uc($encoding);
125
126 binmode(STDOUT, ':raw') if ($encoding ne 'UTF-8');
127 binmode(STDOUT, ':utf8') if ($encoding eq 'UTF-8');
128
129 if (!grep { $format eq $_ } @formats) {
130     die "Please select a supported format.  ".
131         "Right now that means one of [".
132         join('|',@formats). "]\n";
133 }
134
135 if ($format ne 'XML') {
136     my $type = 'MARC::File::' . $format;
137     $type->require;
138 }
139
140 if ($timeout <= 0) {
141     # set default timeout and/or correct silly user who 
142     # supplied a negative timeout; default timeout of
143     # 300 seconds if exporting items determined empirically.
144     $timeout = $holdings ? 300 : 1;
145 }
146
147 OpenSRF::System->bootstrap_client( config_file => $config );
148
149 if (!$idl) {
150     $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
151 }
152
153 Fieldmapper->import(IDL => $idl);
154
155 my $ses = OpenSRF::AppSession->create('open-ils.cstore');
156 OpenILS::Utils::CStoreEditor::init();
157 my $editor = OpenILS::Utils::CStoreEditor->new();
158
159 print <<HEADER if ($format eq 'XML');
160 <?xml version="1.0" encoding="$encoding"?>
161 <collection xmlns='http://www.loc.gov/MARC21/slim'>
162 HEADER
163
164 my %orgs;
165 my %shelves;
166
167 my $flesh = {};
168
169 if ($holdings) {
170     get_bib_locations();
171 }
172
173 my $start = time;
174 my $last_time = time;
175 my %count = ('bib' => 0, 'did' => 0);
176 my $speed = 0;
177
178 if ($all_records) {
179     my $top_record = 0;
180     if ($type eq 'biblio') {
181         $top_record = $editor->search_biblio_record_entry([
182             {deleted => 'f'},
183             {order_by => { 'bre' => 'id DESC' }, limit => 1}
184         ])->[0]->id;
185     } elsif ($type eq 'authority') {
186         $top_record = $editor->search_authority_record_entry([
187             {deleted => 'f'},
188             {order_by => { 'are' => 'id DESC' }, limit => 1}
189         ])->[0]->id;
190     }
191     for (my $i = 0; $i++ < $top_record;) {
192         export_record($i);
193     }
194 } elsif (@library) {
195     my $recids = $editor->json_query({
196         select => { bre => ['id'] },
197         from => { bre => 'acn' },
198         where => {
199             '+bre' => { deleted => 'f' },
200             '+acn' => { 
201                 deleted => 'f', 
202                 owning_lib => {
203                     in => {
204                         select => {'aou' => ['id'] },
205                         from => 'aou',
206                         where => { shortname => { in => \@library } }
207                     } 
208                 }
209             }
210         },
211         distinct => 1,
212         order_by => [{
213             class => 'bre',
214             field => 'id',
215             direction => 'ASC' 
216         }]
217     });
218
219     foreach my $record (@$recids) {
220         export_record($record->{id});
221     }; 
222 } else {
223     while ( my $i = <> ) {
224         export_record($i);
225     }
226 }
227 print "</collection>\n" if ($format eq 'XML');
228
229 $speed = $count{did} / (time - $start);
230 my $time = time - $start;
231 print STDERR <<DONE;
232
233 Exports Attempted : $count{bib}
234 Exports Completed : $count{did}
235 Overall Speed     : $speed
236 Total Time Elapsed: $time seconds
237
238 DONE
239
240 sub export_record {
241     my $id = int(shift);
242
243     my $bib; 
244
245     my $r = $ses->request( "open-ils.cstore.direct.$type.record_entry.retrieve", $id, $flesh );
246     my $s = $r->recv(timeout => $timeout);
247     if (!$s) {
248         warn "\n!!!!! Failed trying to read record $id\n";
249         return;
250     }
251     if ($r->failed) {
252         warn "\n!!!!!! Failed trying to read record $id: " . $r->failed->stringify . "\n";
253         return;
254     }
255     if ($r->timed_out) {
256         warn "\n!!!!!! Timed out trying to read record $id\n";
257         return;
258     }
259     $bib = $s->content;
260     $r->finish;
261
262     $count{bib}++;
263     return unless $bib;
264
265     if ($format eq 'ARE' or $format eq 'BRE') {
266         print OpenSRF::Utils::JSON->perl2JSON($bib);
267         stats();
268         $count{did}++;
269         return;
270     }
271
272     try {
273
274         my $r = MARC::Record->new_from_xml( $bib->marc, $encoding, $format );
275         if ($type eq 'biblio') {
276             add_bib_holdings($bib, $r);
277         }
278
279         if ($replace_001) {
280             my $tcn = $r->field('001');
281             if ($tcn) {
282                 $tcn->update($id);
283             } else {
284                 my $new_001 = MARC::Field->new('001', $id);
285                 $r->insert_fields_ordered($new_001);
286             }
287         }
288
289         if ($format eq 'XML') {
290             my $xml = $r->as_xml_record;
291             $xml =~ s/^<\?.+?\?>$//mo;
292             print $xml;
293         } elsif ($format eq 'UNIMARC') {
294             print $r->as_usmarc;
295         } elsif ($format eq 'USMARC') {
296             print $r->as_usmarc;
297         }
298
299         $count{did}++;
300
301     } otherwise {
302         my $e = shift;
303         warn "\n$e\n";
304         import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire export
305     };
306
307     if ($export_mfhd and $type eq 'biblio') {
308         my $mfhds = $editor->search_serial_record_entry({record => $id, deleted => 'f'});
309         foreach my $mfhd (@$mfhds) {
310             try {
311                 my $r = MARC::Record->new_from_xml( $mfhd->marc, $encoding, $format );
312
313                 if ($format eq 'XML') {
314                     my $xml = $r->as_xml_record;
315                     $xml =~ s/^<\?.+?\?>$//mo;
316                     print $xml;
317                 } elsif ($format eq 'UNIMARC') {
318                     print $r->as_usmarc;
319                 } elsif ($format eq 'USMARC') {
320                     print $r->as_usmarc;
321                 }
322             } otherwise {
323                 my $e = shift;
324                 warn "\n$e\n";
325                 import MARC::File::XML; # reset SAX parser so that one bad record doesn't kill the entire export
326             };
327         }
328     }
329
330     stats() if (! ($count{bib} % 50 ));
331 }
332
333 sub stats {
334     try {
335         no warnings;
336
337         $speed = $count{did} / (time - $start);
338
339         my $speed_now = ($count{did} - $count{did_last}) / (time - $count{time_last});
340         my $cn_speed = $count{cn} / (time - $start);
341         my $cp_speed = $count{cp} / (time - $start);
342
343         printf STDERR "\r  $count{did} of $count{bib} @  \%0.4f/s ttl / \%0.4f/s rt ".
344                 "($count{cn} CNs @ \%0.4f/s :: $count{cp} CPs @ \%0.4f/s)\r",
345                 $speed,
346                 $speed_now,
347                 $cn_speed,
348                 $cp_speed;
349     } otherwise {};
350     $count{did_last} = $count{did};
351     $count{time_last} = time;
352 }
353
354 sub get_bib_locations {
355     print STDERR "Retrieving Org Units ... ";
356     my $r = $ses->request( 'open-ils.cstore.direct.actor.org_unit.search', { id => { '!=' => undef } } );
357
358     while (my $o = $r->recv) {
359         die $r->failed->stringify if ($r->failed);
360         $o = $o->content;
361         last unless ($o);
362         $orgs{$o->id} = $o;
363     }
364     $r->finish;
365     print STDERR "OK\n";
366
367     print STDERR "Retrieving Shelving locations ... ";
368     $r = $ses->request( 'open-ils.cstore.direct.asset.copy_location.search', { id => { '!=' => undef } } );
369
370     while (my $s = $r->recv) {
371         die $r->failed->stringify if ($r->failed);
372         $s = $s->content;
373         last unless ($s);
374         $shelves{$s->id} = $s;
375     }
376     $r->finish;
377     print STDERR "OK\n";
378
379     $flesh = { flesh => 2, flesh_fields => { bre => [ 'call_numbers' ], acn => [ 'copies' ] } };
380 }
381
382 sub add_bib_holdings {
383     my $bib = shift;
384     my $r = shift;
385
386     my $cn_list = $bib->call_numbers;
387     if ($cn_list && @$cn_list) {
388
389         $count{cn} += @$cn_list;
390     
391         my $cp_list = [ map { @{ $_->copies } } @$cn_list ];
392         if ($cp_list && @$cp_list) {
393
394             my %cn_map;
395             push @{$cn_map{$_->call_number}}, $_ for (@$cp_list);
396                             
397             for my $cn ( @$cn_list ) {
398                 my $cn_map_list = $cn_map{$cn->id};
399
400                 for my $cp ( @$cn_map_list ) {
401                     $count{cp}++;
402                             
403                     $r->append_fields(
404                         MARC::Field->new(
405                             852, '4', '', 
406                             a => $location,
407                             b => $orgs{$cn->owning_lib}->shortname,
408                             b => $orgs{$cp->circ_lib}->shortname,
409                             c => $shelves{$cp->location}->name,
410                             j => $cn->label,
411                             ($cp->circ_modifier ? ( g => $cp->circ_modifier ) : ()),
412                             p => $cp->barcode,
413                             ($cp->price ? ( y => $dollarsign.$cp->price ) : ()),
414                             ($cp->copy_number ? ( t => $cp->copy_number ) : ()),
415                             ($cp->ref eq 't' ? ( x => 'reference' ) : ()),
416                             ($cp->holdable eq 'f' ? ( x => 'unholdable' ) : ()),
417                             ($cp->circulate eq 'f' ? ( x => 'noncirculating' ) : ()),
418                             ($cp->opac_visible eq 'f' ? ( x => 'hidden' ) : ()),
419                         )
420                     );
421
422                     stats() if (! ($count{cp} % 100 ));
423                 }
424             }
425         }
426     }
427 }