]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/support-scripts/marc_export.in
LP#1539088: marc_export prints "Waiting for input" when non-interactive
[working/Evergreen.git] / Open-ILS / src / support-scripts / marc_export.in
1 #!/usr/bin/perl
2 # ---------------------------------------------------------------
3 # Copyright © 2013 Merrimack Valley Library Consortium
4 # Jason Stephenson <jstephenson@mvlc.org>
5 #
6 # This program is part of Evergreen; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License as
8 # published by the Free Software Foundation; either version 2 of the
9 # 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 use strict;
17 use warnings;
18 use OpenILS::Utils::Fieldmapper;
19 use OpenILS::Application::AppUtils;
20 use OpenSRF::Utils::JSON;
21 use MARC::Field;
22 use MARC::Record;
23 use MARC::File::XML (BinaryEncoding => 'UTF-8');
24 use Date::Manip::Date;
25 my $U = 'OpenILS::Application::AppUtils';
26
27 binmode(STDERR, ':utf8');
28
29 package Marque;
30
31 our $config = Marque::Config->new();
32 Fieldmapper->import(IDL => $config->option_value('xml-idl'));
33
34 # Look for passed in ids:
35 my @ids = ();
36 if ($config->need_ids()) {
37     print STDERR "Waiting for input\n" if (-t);
38     while (my $i = <>) {
39         push @ids, $i if ($i =~ /^\s*[0-9]+\s*$/);
40     }
41 }
42
43 my $exporter;
44 if ($config->option_value('type') eq 'authority') {
45     $exporter = Marque::Authority->new(\@ids);
46 } else {
47     $exporter = Marque::Biblio->new(\@ids);
48 }
49
50 Marque::Output::output($exporter);
51
52 # ------------------------------------------------------------------
53 package Marque::Config;
54
55 use Getopt::Long;
56 use List::MoreUtils qw(none);
57 use OpenSRF::System;
58 use OpenSRF::Utils::SettingsClient;
59
60 use constant FORMATS => qw(USMARC UNIMARC XML BRE ARE);
61 use constant STORES => qw(reporter cstore storage);
62 use constant TYPES => qw(authority biblio);
63
64
65 sub new {
66     my $class = shift;
67
68     my $self = {};
69
70     # For command line options.
71     my %opts;
72
73     # set some default values
74     $opts{'format'} = 'USMARC';
75     $opts{'encoding'} = 'MARC8';
76     $opts{'type'} = 'biblio';
77     $opts{'money'} = '$';
78     $opts{'timeout'} = 0;
79     $opts{'config'} = '@sysconfdir@/opensrf_core.xml';
80     $opts{'store'} = 'reporter';
81
82     GetOptions(\%opts,
83                'help',
84                'items',
85                'mfhd',
86                'all',
87                'replace_001',
88                'location=s',
89                'money=s',
90                'config=s',
91                'format=s',
92                'type=s',
93                'xml-idl=s',
94                'encoding=s',
95                'timeout=i',
96                'library=s@',
97                'since=s',
98                'store=s',
99                'debug');
100
101     if ($opts{help}) {
102         print <<"HELP";
103 This script exports MARC authority, bibliographic, and serial holdings
104 records from an Evergreen database. 
105
106 Input to this script can consist of a list of record IDs, with one record ID
107 per line, corresponding to the record ID in the Evergreen database table of
108 your requested record type.
109
110 Alternately, passing the --all option will attempt to export all records of
111 the specified type from the Evergreen database. The --all option starts at
112 record ID 1 and increments the ID by 1 until the largest ID in the database
113 is retrieved. This may not be very efficient for databases with large gaps
114 in their ID sequences.
115
116 Usage: $0 [options]
117  --help or -h       This screen.
118  --config or -c     Configuration file [@sysconfdir@/opensrf_core.xml]
119  --format or -f     Output format (USMARC, UNIMARC, XML, BRE, ARE) [USMARC]
120  --encoding or -e   Output encoding (UTF-8, ISO-8859-?, MARC8) [MARC8]
121  --xml-idl or -x    Location of the IDL XML
122  --timeout          Remains for backward compatibility. No longer used.
123  --type or -t       Record type (BIBLIO, AUTHORITY) [BIBLIO]
124  --all or -a        Export all records; ignores input list
125  --replace_001      Replace the 001 field value with the record ID
126  --store            Use the given storage backend to connect to the database.
127                     Choices are (reporter, cstore, storage) [reporter]
128  --since            Export records modified since a certain date and time.
129
130  Additional options for type = 'BIBLIO':
131  --items or -i      Include items (holdings) in the output
132  --money            Currency symbol to use in item price field [\$]
133  --mfhd             Export serial MFHD records for associated bib records
134                     Not compatible with --format=BRE
135  --location or -l   MARC Location Code for holdings from
136                     http://www.loc.gov/marc/organizations/orgshome.html
137  --library          Export the bibliographic records that have attached
138                     holdings for the listed library or libraries as
139                     identified by shortname
140
141 Examples:
142
143 To export a set of USMARC records in a file named "output_file" based on the
144 IDs contained in a file named "list_of_ids":
145   cat list_of_ids | $0 > output_file
146
147 To export a set of MARC21XML authority records in a file named "output.xml"
148 for all authority records in the database:
149   $0 --format XML --type AUTHORITY --all > output.xml
150
151 To export a set of USMARC bibliographic records encoded in UTF-8 in a file
152 named "sys1_bibs.mrc" based on records which have attached callnumbers for the
153 libraries with the short names "BR1" and "BR2":
154
155   $0 --library BR1 --library BR2 --encoding UTF-8 > sys1_bibs.mrc
156
157 HELP
158         exit;
159     }
160
161     OpenSRF::System->bootstrap_client( config_file => $opts{config} );
162     my $sclient = OpenSRF::Utils::SettingsClient->new();
163     unless ($opts{'xml-idl'}) {
164         $opts{'xml-idl'} = $sclient->config_value('IDL');
165     }
166
167     # Validate some of the settings.
168     if ($opts{all} && $opts{library}) {
169         die('Incompatible arguments: you cannot combine a request for all ' .
170                 'records with a request for records by library');
171     }
172     if ($opts{all} && $opts{since}) {
173         die('Incompatible arguments: you cannot combine a request for all ' .
174                 'records with a request for records added or changed since a certain date');
175     }
176     $opts{type} = lc($opts{type});
177     if (none {$_ eq $opts{type}} (TYPES)) {
178         die "Please select a supported type.  ".
179             "Right now that means one of [".
180                 join('|',(FORMATS)). "]\n";
181     }
182     $opts{format} = uc($opts{format});
183     if (none {$_ eq $opts{format}} (FORMATS)) {
184         die "Please select a supported format.  ".
185             "Right now that means one of [".
186                 join('|',(FORMATS)). "]\n";
187     }
188
189     if ($opts{format} eq 'ARE' && $opts{type} ne 'authority') {
190         die "Format ARE is not compatible with type " . $opts{type};
191     }
192     if ($opts{format} eq 'BRE' && $opts{type} ne 'biblio') {
193         die "Format BRE is not compatible with type " . $opts{type};
194     }
195     if ($opts{format} eq 'BRE' && $opts{items}) {
196         die "Format BRE is not compatible with exporting holdings."
197     }
198
199     if ($opts{mfhd}) {
200         if ($opts{type} ne 'biblio') {
201             die "MFHD export only works with bibliographic records.";
202         } elsif ($opts{format} eq 'BRE') {
203             die "MFHD export incompatible with format BRE.";
204         }
205     }
206
207     $opts{store} = lc($opts{store});
208     if (none {$_ eq $opts{store}} (STORES)) {
209         die "Please select a supported store.  ".
210             "Right now that means one of [".
211                 join('|',(STORES)). "]\n";
212     } else {
213         my $app;
214         if ($opts{store} eq 'reporter') {
215             $app = 'open-ils.reporter-store';
216         } else {
217             $app = 'open-ils.' . $opts{store};
218         }
219         if ($app eq 'open-ils.storage') {
220             $self->{dbsettings} = $sclient->config_value(
221                 apps => $app => app_settings => databases => 'database');
222         } else {
223             $self->{dbsettings} = $sclient->config_value(
224                 apps => $app => app_settings => 'database');
225         }
226     }
227     $opts{encoding} = uc($opts{encoding});
228
229     $self->{'options'} = \%opts;
230     bless $self, $class;
231     return $self;
232 }
233
234 sub option_value {
235     my ($self, $option) = @_;
236     return $self->{options}->{$option};
237 }
238
239 sub database_settings {
240     my $self = shift;
241     return $self->{dbsettings};
242 }
243
244 sub need_ids {
245     my $self = shift;
246     my $rv = 1;
247
248     $rv = 0 if ($self->{options}->{all});
249     $rv = 0 if ($self->{options}->{since});
250     $rv = 0 if ($self->{options}->{library});
251
252     return $rv;
253 }
254
255 # ------------------------------------------------------------------
256 # This package exists to get a connection to the database.  Since
257 # we'll need one for both biblio records and authorities, we've made a
258 # single subpackage with a function so that we don't have to duplicate
259 # code.
260 package Marque::Connector;
261
262 use DBI;
263
264 # Pass a Marque::Config object's database_settings return value into
265 # this to get a DBI connection.
266 # ex:
267 # my $db = Marque::Connector::connect($config->database_settings);
268 sub connect {
269     my $args = shift;
270
271     # Build a connect string from the args.
272     my $connect_string = 'DBI:Pg:';
273     $connect_string .= 'dbname=' . $args->{db} . ';';
274     $connect_string .= 'host=' . $args->{host} . ';';
275     $connect_string .= 'port=' . $args->{port};
276
277     my $db_handle = DBI->connect($connect_string,
278                                  $args->{user}, $args->{pw});
279     return $db_handle;
280 }
281
282 # A function to get the date into a format better for PostgreSQL.
283 sub db_date {
284     my $input = shift;
285     my $date;
286     if (ref($input) eq 'Date::Manip::Date') {
287         $date = $input;
288     } else {
289         $date = Date::Manip::Date->new();
290         if ($date->parse($input)) {
291             die "Can't parse date $input";
292         }
293     }
294     return $date->printf("%Y-%m-%dT%H:%M:%S%z");
295  }
296
297 # ------------------------------------------------------------------
298 # You would typically have the next two packages inherit from a common
299 # superclass, but ineritance doesn't seem to work when all packages
300 # are in single file, so we have some duplicated code between these
301 # two.
302
303 # Get bibliographic records from the database.
304 package Marque::Biblio;
305
306 sub new {
307     my $class = shift;
308     my $idlist = shift;
309     my $self = {idlist => $idlist};
310     $self->{handle} = Marque::Connector::connect(
311         $Marque::config->database_settings);
312     $self->{since_date} = Date::Manip::Date->new;
313     $self->{since_date}->parse($Marque::config->option_value('since'));
314
315     # We need multiple fieldmapper classes depending on our
316     # options. We'll just get the information that we'll need for them
317     # all right here instead of only fetching the information when
318     # needed.
319     $self->{breClass} = Fieldmapper::class_for_hint('bre');
320     $self->{acnClass} = Fieldmapper::class_for_hint('acn');
321     $self->{acpClass} = Fieldmapper::class_for_hint('acp');
322     $self->{sreClass} = Fieldmapper::class_for_hint('sre');
323
324     # Make an arrayref of shortname ids if the library option was
325     # specified:
326     $self->{libs} = [];
327     if ($Marque::config->option_value('library')) {
328         # This is done not only for speed, but to prevent SQL injection.
329         my $sth = $self->{handle}->prepare('select id from actor.org_unit where shortname=any(?::text[])');
330         if ($sth->execute($Marque::config->option_value('library'))) {
331             my $r = $sth->fetchall_arrayref();
332             my @ids = map {$_->[0]} @{$r};
333             $self->{libs} = \@ids;
334             $sth->finish();
335         }
336     }
337
338     bless $self, $class;
339     return $self;
340 }
341
342 sub build_query {
343     my $self = shift;
344
345     # Get the field names and tables for our classes. We add the fully
346     # qualified table names to the fields so that the joins will work.
347     my $breTable = $self->{breClass}->Table();
348     my @breFields = map {$breTable . '.' . $_} $self->{breClass}->real_fields();
349     my $acnTable = $self->{acnClass}->Table();
350     my $acpTable = $self->{acpClass}->Table();
351
352     # Now we build the query in pieces:
353
354     # We always select the bre fields:
355     my $select = 'select distinct ' . join(',', @breFields);
356     # We always use the bre table.
357     my $from = "from $breTable";
358
359     # If have the libraries or items options, we need to join the
360     # asset.call_number table. If we have both, this variable checks
361     # that it has already been joined so we don't create an invalid
362     # query.
363     my $acn_joined = 0;
364     # Join to the acn table as needed for the library option.
365     if (@{$self->{libs}}) {
366         $acn_joined = 1;
367         $from .= <<ACN_JOIN;
368
369 join $acnTable on $acnTable.record = $breTable.id
370 and $acnTable.owning_lib in (
371 ACN_JOIN
372         $from .= join(',', @{$self->{libs}}) . ")";
373         $from .= "\nand $acnTable.deleted = 'f'" unless ($Marque::config->option_value('since'));
374     }
375
376     if ($Marque::config->option_value('items')) {
377         unless ($acn_joined) {
378             $from .= "\njoin $acnTable on $acnTable.record = $breTable.id";
379             $from .= "\nand $acnTable.deleted = 'f'" unless ($Marque::config->option_value('since'));
380         }
381         $from .= "\njoin $acpTable on $acpTable.call_number = $acnTable.id";
382         $from .= "\nand $acpTable.deleted = 'f'" unless ($Marque::config->option_value('since'));
383     }
384
385     # The where really depends on a few options:
386     my $where = "where $breTable.id > 0 and ";
387     # We fill in the where as necessary.
388     if ($self->{idlist} && @{$self->{idlist}}) {
389         $where .= "$breTable.id in (" . join(',', @{$self->{idlist}}) . ')';
390     } elsif ($Marque::config->option_value('since')) {
391         my $since_str = Marque::Connector::db_date($self->{since_date});
392         $where .= "($breTable.edit_date > '$since_str'";
393         $where .= " or $breTable.create_date > '$since_str')";
394     } else {
395         # We want all non-deleted records.
396         $where .= "$breTable.deleted = 'f'";
397     }
398
399     $self->{query} = $select . "\n" . $from . "\n" . $where;
400 }
401
402 sub execute_query {
403     my $self = shift;
404     $self->build_query() unless ($self->{query});
405     $self->{sth} = $self->{handle}->prepare($self->{query});
406     return $self->{sth}->execute;
407 }
408
409 sub next {
410     my $self = shift;
411     my $output;
412
413     # $r holds the record object, either sre or bre.  $marc holds the
414     # current record's MARC, either sre.marc or bre.marc
415     my ($r,$marc);
416     # If we have the mfhd option and we've previously retrieved some
417     # sres, then we output one of the retrieved sres for each call
418     # until we run out.  These sres "go with" the previous bib record.
419     if ($Marque::config->option_value('mfhd') && $self->{mfhds} && @{$self->{mfhds}}) {
420         $r = shift(@{$self->{mfhds}});
421         eval {
422             local $SIG{__WARN__} = sub {
423                 my $message = "Warning from serial record " . $r->id() . ": "
424                     . shift;
425                 warn($message);
426             };
427             $marc = MARC::Record->new_from_xml($r->marc(),
428                                                $Marque::config->option_value('encoding'),
429                                                $Marque::config->option_value('format'));
430         };
431         if ($@) {
432             warn("Error in serial record " . $r->id() . ": $@");
433             import MARC::File::XML; # Reset SAX Parser.
434             return $self->next();
435         }
436     } else {
437         my $data = $self->{sth}->fetchrow_hashref;
438         if ($data) {
439             $r = $self->{breClass}->from_bare_hash($data);
440             if ($Marque::config->option_value('format') eq 'BRE') {
441                 $output = OpenSRF::Utils::JSON->perl2JSON($r);
442             } else {
443                 eval {
444                     local $SIG{__WARN__} = sub {
445                         my $message = "Warning from bibliographic record " . $r->id() . ": "
446                             . shift;
447                         warn($message);
448                     };
449                     $marc = MARC::Record->new_from_xml($r->marc(),
450                                                        $Marque::config->option_value('encoding'),
451                                                        $Marque::config->option_value('format'));
452                 };
453                 if ($@) {
454                     warn("Error in bibliographic record " . $r->id() . ": $@");
455                     import MARC::File::XML; # Reset SAX Parser.
456                     return $self->next();
457                 }
458                 if ($Marque::config->option_value('replace_001')) {
459                     my $tcn = $marc->field('001');
460                     if ($tcn) {
461                         $tcn->update($r->id());
462                     } else {
463                         $tcn = MARC::Field->new('001', $r->id());
464                         $marc->insert_fields_ordered($tcn);
465                     }
466                 }
467                 if ($Marque::config->option_value('items')) {
468                     my @acps = $self->acps_for_bre($r);
469                     foreach my $acp (@acps) {
470                         next unless ($acp);
471                         my $location = $Marque::config->option_value('location');
472                         my $price = ($acp->price() ? $Marque::config->option_value('money').$acp->price() : '');
473                         $marc->insert_grouped_field(
474                             MARC::Field->new(
475                                 '852', '4', ' ',
476                                 ($location ? ('a' => $location) : ()),
477                                 b => $acp->call_number()->owning_lib()->shortname(),
478                                 b => $acp->circ_lib()->shortname(),
479                                 c => $acp->location()->name(),
480                                 j => $acp->call_number()->label(),
481                                 ($acp->circ_modifier() ? (g => $acp->circ_modifier()) : ()),
482                                 p => $acp->barcode(),
483                                 ($price ? (y => $price) : ()),
484                                 ($acp->copy_number() ? (t => $acp->copy_number()) : ()),
485                                 ($U->is_true($acp->ref()) ? (x => 'reference') : ()),
486                                 (!$U->is_true($acp->holdable()) ? (x => 'unholdable') : ()),
487                                 (!$U->is_true($acp->circulate()) ? (x => 'noncirculating') : ()),
488                                 (!$U->is_true($acp->opac_visible()) ? (x => 'hidden') : ())
489                             ));
490                     }
491                 }
492                 if ($Marque::config->option_value('mfhd')) {
493                     $self->{mfhds} = [$self->sres_for_bre($r)];
494                 }
495             }
496         }
497     }
498     # Common stuff that doesn't depend on record type.
499     if ($marc) {
500         if ($Marque::config->option_value('since')) {
501             my $leader = $marc->leader();
502             if ($U->is_true($r->deleted())) {
503                 substr($leader, 5, 1) = 'd';
504                 $marc->leader($leader);
505             } else {
506                 my $create_date = Date::Manip::Date->new;
507                 $create_date->parse($r->create_date());
508                 my $edit_date = Date::Manip::Date->new;
509                 $edit_date->parse($r->edit_date());
510                 if ($self->{since_date}->cmp($create_date) < 0) {
511                     substr($leader, 5, 1) = 'n';
512                     $marc->leader($leader);
513                 } elsif ($self->{since_date}->cmp($edit_date) < 0) {
514                     substr($leader, 5, 1) = 'c';
515                     $marc->leader($leader);
516                 }
517             }
518         }
519         if ($Marque::config->option_value('format') eq 'XML') {
520             eval {
521                 local $SIG{__WARN__} = sub {
522                     my $message = "Warning from bibliographic record " . $r->id() . ": "
523                         . shift;
524                     warn($message);
525                 };
526                 $output = $marc->as_xml_record;
527                 $output =~ s/^<\?.+?\?>$//mo;
528             };
529             if ($@) {
530                 warn("Error in bibliographic record " . $r->id() . ": $@");
531                 return $self->next();
532             }
533         } else {
534             eval {
535                 local $SIG{__WARN__} = sub {
536                     my $message = "Warning from bibliographic record " . $r->id() . ": "
537                         . shift;
538                     warn($message);
539                 };
540                 $output = $marc->as_usmarc;
541             };
542             if ($@) {
543                 warn("Error in bibliographic record " . $r->id() . ": $@");
544                 return $self->next();
545             }
546         }
547     }
548     return $output;
549 }
550
551 # Returns a list of aou objects in an array.
552 sub orgs {
553     my $self = shift;
554     unless ($self->{orgs} && @{$self->{orgs}}) {
555         my $fmClass = Fieldmapper::class_for_hint('aou');
556         my @classFields = $fmClass->real_fields();
557         my $classTable = $fmClass->Table();
558         my $query = 'select ' . join(',', @classFields);
559         $query .= "\nfrom $classTable";
560         my $sth = $self->{handle}->prepare($query);
561         if ($sth->execute()) {
562             my $result = $sth->fetchall_arrayref({});
563             my @orgs = map {$fmClass->from_bare_hash($_)} @{$result};
564             $self->{orgs} = \@orgs;
565         } else {
566             $self->{orgs} = [];
567         }
568     }
569     return @{$self->{orgs}};
570 }
571
572 # Returns an array of acpl objects.
573 sub shelves {
574     my $self = shift;
575
576     unless ($self->{shelves} && @{$self->{shelves}}) {
577         my $fmClass = Fieldmapper::class_for_hint('acpl');
578         my @classFields = $fmClass->real_fields();
579         my $classTable = $fmClass->Table();
580         my $query = 'select ' . join(',', @classFields);
581         $query .= "\nfrom $classTable";
582         my $result = $self->{handle}->selectall_arrayref($query, {Slice=>{}});
583         my @shelves = map {$fmClass->from_bare_hash($_)} @{$result};
584         $self->{shelves} = \@shelves;
585     }
586
587     return @{$self->{shelves}};
588 }
589
590 # Returns an array of acn objects for a given bre object or id.
591 sub acns_for_bre {
592     my $self = shift;
593     my $bre = shift;
594     $bre = $bre->id() if (ref($bre));
595
596     unless ($self->{acnHandle}) {
597         my $query = "select " . join(',', $self->{acnClass}->real_fields());
598         $query .= "\nfrom " . $self->{acnClass}->Table();
599         $query .= "\nwhere record = ?";
600         if (@{$self->{libs}}) {
601             $query .= "\nand owning_lib in (";
602             $query .= join(',', @{$self->{libs}}) . ")";
603         }
604         $query .= "\nand deleted = 'f'" unless($Marque::config->option_value('since'));
605         $self->{acnHandle} = $self->{handle}->prepare($query);
606     }
607
608     if ($self->{acnHandle}->execute($bre)) {
609         my $result = $self->{acnHandle}->fetchall_arrayref({});
610         return map {$self->{acnClass}->from_bare_hash($_)} @{$result};
611     }
612
613     # If for some reason, we don't find anything.
614     return undef;
615 }
616
617 # Returns an array of acp objects for a given bre object or id.
618 sub acps_for_bre {
619     my $self = shift;
620     my $bre = shift;
621     $bre = $bre->id() if (ref($bre));
622
623     my @orgs = $self->orgs();
624     my @locations = $self->shelves();
625
626     my @acns = $self->acns_for_bre($bre);
627     if (@acns) {
628         my $query = 'select ' . join(',', $self->{acpClass}->real_fields());
629         $query .= "\nfrom " . $self->{acpClass}->Table();
630         $query .= "\nwhere call_number in (";
631         $query .= join(',', map {$_->id()} @acns);
632         $query .= ")";
633         $query .= "\nand deleted = 'f'" unless ($Marque::config->option_value('since'));
634         my $result = $self->{handle}->selectall_arrayref($query, {Slice=>{}});
635         if ($result && @{$result}) {
636             my @acps = map {$self->{acpClass}->from_bare_hash($_)} @{$result};
637             foreach (@acps) {
638                 my $cn = $_->call_number();
639                 my $clib = $_->circ_lib();
640                 my $loc = $_->location();
641                 my ($org) = grep {$_->id() == $clib} @orgs;
642                 my ($acn) = grep {$_->id() == $cn} @acns;
643                 my ($location) = grep {$_->id() == $loc} @locations;
644                 my $olib = $acn->owning_lib();
645                 my ($owner) = grep {$_->id() == $olib} @orgs;
646                 $acn->owning_lib($owner);
647                 $_->call_number($acn);
648                 $_->circ_lib($org);
649                 $_->location($location);
650             }
651             return @acps;
652         }
653     }
654
655     # If for some reason, we don't find anything.
656     return undef;
657 }
658
659 # Retreive an array for sre objects when the --mfhd option is used.
660 sub sres_for_bre {
661     my $self = shift;
662     my $bre = shift;
663     $bre = $bre->id() if (ref($bre));
664     my @sres;
665     # Build a query to retrieve SREs when the MFHD option is passed.
666     if ($Marque::config->option_value('mfhd')) {
667         # Create a persistent handle as needed.
668         unless ($self->{sreSth}) {
669             my $query = "select " . join(',', $self->{sreClass}->real_fields());
670             $query .= "\nfrom " . $self->{sreClass}->Table();
671             $query .= "\nwhere record = ?";
672             $query .= "\nand deleted = 'f'" unless ($Marque::config->option_value('since'));
673             $self->{sreSth} = $self->{handle}->prepare($query);
674         }
675         if ($self->{sreSth}->execute($bre)) {
676             while (my $data = $self->{sreSth}->fetchrow_hashref) {
677                 push @sres, $self->{sreClass}->from_bare_hash($data);
678             }
679             $self->{sreSth}->finish; # Sometimes DBI complains.
680         }
681     }
682     # May be empty.
683     return @sres;
684 }
685
686 # Get authority records from the database.
687 package Marque::Authority;
688
689 sub new {
690     my $class = shift;
691     my $idlist = shift;
692     my $self = {idlist => $idlist};
693     $self->{handle} = Marque::Connector::connect(
694         $Marque::config->database_settings);
695     $self->{fmClass} = Fieldmapper::class_for_hint('are');
696     $self->{since_date} = Date::Manip::Date->new;
697     $self->{since_date}->parse($Marque::config->option_value('since'));
698     bless $self, $class;
699     return $self;
700 }
701
702 sub build_query {
703     my $self = shift;
704
705     # Get the information for are object from the Fieldmapper:
706     my @fields  = $self->{fmClass}->real_fields();
707     my $table = $self->{fmClass}->Table();
708
709     # Build the actual query.
710     my $select = "select " . join(',', @fields);
711     my $from = "from $table";
712     my $where = 'where ';
713
714     # If we have an idlist, we pretty much ignore anything else.
715     if ($self->{idlist} && @{$self->{idlist}}) {
716         $where .= 'id in (' . join(',', @{$self->{idlist}}) . ')';
717     } elsif ($Marque::config->option_value('since')) {
718         my $since_str = Marque::Connector::db_date($self->{since_date});
719         $where .= "edit_date > '$since_str'";
720         $where .= " or create_date > '$since_str'";
721     } else {
722         # We want all non-deleted records.
723         $where .= "deleted = 'f'";
724     }
725
726     $self->{query} = $select . "\n" . $from . "\n" . $where;
727 }
728
729 sub execute_query {
730     my $self = shift;
731     $self->build_query() unless ($self->{query});
732     $self->{sth} = $self->{handle}->prepare($self->{query});
733     return $self->{sth}->execute;
734 }
735
736 sub next {
737     my $self = shift;
738     my $output;
739     my $data = $self->{sth}->fetchrow_hashref;
740
741     if ($data) {
742         my $format = $Marque::config->option_value('format');
743         my $r = $self->{fmClass}->from_bare_hash($data);
744         if ($format eq 'ARE') {
745             $output = OpenSRF::Utils::JSON->perl2JSON($r);
746         } else {
747             my $marc;
748             eval {
749                 local $SIG{__WARN__} = sub {
750                     my $message = "Warning from authority record " . $r->id() . ": "
751                         . shift;
752                     warn($message);
753                 };
754                 $marc = MARC::Record->new_from_xml($r->marc(),
755                                                 $Marque::config->option_value('encoding'),
756                                                 $Marque::config->option_value('format'));
757             };
758             if ($@) {
759                 warn("Error in authority record " . $r->id() . ": $@");
760                 import MARC::File::XML; # Reset SAX Parser.
761                 return $self->next();
762             }
763             if ($Marque::config->option_value('replace_001')) {
764                 my $tcn = $marc->field('001');
765                 if ($tcn) {
766                     $tcn->update($r->id());
767                 } else {
768                     $tcn = MARC::Field->new('001', $r->id());
769                     $marc->insert_fields_ordered($tcn);
770                 }
771             }
772             if ($Marque::config->option_value('since')) {
773                 my $leader = $marc->leader();
774                 if ($U->is_true($r->deleted())) {
775                     substr($leader, 5, 1) = 'd';
776                     $marc->leader($leader);
777                 } else {
778                     my $create_date = Date::Manip::Date->new;
779                     $create_date->parse($r->create_date());
780                     my $edit_date = Date::Manip::Date->new;
781                     $edit_date->parse($r->edit_date());
782                     if ($self->{since_date}->cmp($create_date) < 0) {
783                         substr($leader, 5, 1) = 'n';
784                         $marc->leader($leader);
785                     } elsif ($self->{since_date}->cmp($edit_date) < 0) {
786                         substr($leader, 5, 1) = 'c';
787                         $marc->leader($leader);
788                     }
789                 }
790             }
791             if ($Marque::config->option_value('format') eq 'XML') {
792                 eval {
793                     local $SIG{__WARN__} = sub {
794                         my $message = "Warning from authority record " . $r->id() . ": "
795                             . shift;
796                         warn($message);
797                     };
798                     $output = $marc->as_xml_record;
799                     $output =~ s/^<\?.+?\?>$//mo;
800                 };
801                 if ($@) {
802                     warn("Error in authority record " . $r->id() . ": $@");
803                     return $self->next();
804                 }
805             } else {
806                 eval {
807                     local $SIG{__WARN__} = sub {
808                         my $message = "Warning from authority record " . $r->id() . ": "
809                             . shift;
810                         warn($message);
811                     };
812                     $output = $marc->as_usmarc;
813                 };
814                 if ($@) {
815                     warn("Error in authority record " . $r->id() . ": $@");
816                     return $self->next();
817                 }
818             }
819         }
820     }
821
822     return $output;
823 }
824
825 # ------------------------------------------------------------------
826 # Since the ultimate output is largely independent of the type of the
827 # records, we use a single subpackage to group our output routines.
828 package Marque::Output;
829
830 sub output {
831     my $extractor = shift;
832     if ($extractor->execute_query) {
833         if ($Marque::config->option_value('encoding') eq 'UTF-8') {
834             binmode(STDOUT, ':utf8');
835         } else {
836             binmode(STDOUT, ':raw');
837         }
838
839         &preamble;
840         while (my $output = $extractor->next()) {
841             print $output;
842         }
843         &postamble;
844     } else {
845         print STDERR $extractor->{query} if ($Marque::config->option_value('debug'));
846         die "Database query failed!";
847     }
848 }
849
850 sub preamble {
851     if ($Marque::config->option_value('format') eq 'XML') {
852         my $encoding = $Marque::config->option_value('encoding');
853         print <<PREAMBLE;
854 <?xml version="1.0" encoding="$encoding"?>
855 <collection xmlns='http://www.loc.gov/MARC21/slim'>
856 PREAMBLE
857     }
858 }
859
860 sub postamble {
861     if ($Marque::config->option_value('format') eq 'XML') {
862         print "</collection>\n";
863     }
864 }
865
866 1;