]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/support-scripts/marc_export.in
LP1223903 - Rewrite marc_export.in in support-scripts.
[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";
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             $marc = MARC::Record->new_from_xml($r->marc(),
423                                                $Marque::config->option_value('encoding'),
424                                                $Marque::config->option_value('format'));
425         };
426         if ($@) {
427             print STDERR "Error in serial record " . $r->id() . "\n";
428             print STDERR "$@\n";
429             import MARC::File::XML; # Reset SAX Parser.
430             return $self->next();
431         }
432     } else {
433         my $data = $self->{sth}->fetchrow_hashref;
434         if ($data) {
435             $r = $self->{breClass}->from_bare_hash($data);
436             if ($Marque::config->option_value('format') eq 'BRE') {
437                 $output = OpenSRF::Utils::JSON->perl2JSON($r);
438             } else {
439                 eval {
440                     $marc = MARC::Record->new_from_xml($r->marc(),
441                                                        $Marque::config->option_value('encoding'),
442                                                        $Marque::config->option_value('format'));
443                 };
444                 if ($@) {
445                     print STDERR "Error in bibliograpic record " . $r->id() . "\n";
446                     print STDERR "$@\n";
447                     import MARC::File::XML; # Reset SAX Parser.
448                     return $self->next();
449                 }
450                 if ($Marque::config->option_value('replace_001')) {
451                     my $tcn = $marc->field('001');
452                     if ($tcn) {
453                         $tcn->update($r->id());
454                     } else {
455                         $tcn = MARC::Field->new('001', $r->id());
456                         $marc->insert_fields_ordered($tcn);
457                     }
458                 }
459                 if ($Marque::config->option_value('items')) {
460                     my @acps = $self->acps_for_bre($r);
461                     foreach my $acp (@acps) {
462                         next unless ($acp);
463                         my $location = $Marque::config->option_value('location');
464                         my $price = ($acp->price() ? $Marque::config->option_value('money').$acp->price() : '');
465                         $marc->insert_grouped_field(
466                             MARC::Field->new(
467                                 '852', '4', ' ',
468                                 ($location ? ('a' => $location) : ()),
469                                 b => $acp->call_number()->owning_lib()->shortname(),
470                                 b => $acp->circ_lib()->shortname(),
471                                 c => $acp->location()->name(),
472                                 j => $acp->call_number()->label(),
473                                 ($acp->circ_modifier() ? (g => $acp->circ_modifier()) : ()),
474                                 p => $acp->barcode(),
475                                 ($price ? (y => $price) : ()),
476                                 ($acp->copy_number() ? (t => $acp->copy_number()) : ()),
477                                 ($U->is_true($acp->ref()) ? (x => 'reference') : ()),
478                                 (!$U->is_true($acp->holdable()) ? (x => 'unholdable') : ()),
479                                 (!$U->is_true($acp->circulate()) ? (x => 'noncirculating') : ()),
480                                 (!$U->is_true($acp->opac_visible()) ? (x => 'hidden') : ())
481                             ));
482                     }
483                 }
484                 if ($Marque::config->option_value('mfhd')) {
485                     $self->{mfhds} = [$self->sres_for_bre($r)];
486                 }
487             }
488         }
489     }
490     # Common stuff that doesn't depend on record type.
491     if ($marc) {
492         if ($Marque::config->option_value('since')) {
493             my $leader = $marc->leader();
494             if ($U->is_true($r->deleted())) {
495                 substr($leader, 5, 1) = 'd';
496                 $marc->leader($leader);
497             } else {
498                 my $create_date = Date::Manip::Date->new;
499                 $create_date->parse($r->create_date());
500                 my $edit_date = Date::Manip::Date->new;
501                 $edit_date->parse($r->edit_date());
502                 if ($self->{since_date}->cmp($create_date) < 0) {
503                     substr($leader, 5, 1) = 'n';
504                     $marc->leader($leader);
505                 } elsif ($self->{since_date}->cmp($edit_date) < 0) {
506                     substr($leader, 5, 1) = 'c';
507                     $marc->leader($leader);
508                 }
509             }
510         }
511         if ($Marque::config->option_value('format') eq 'XML') {
512             $output = $marc->as_xml_record;
513             $output =~ s/^<\?.+?\?>$//mo;
514         } else {
515             $output = $marc->as_usmarc;
516         }
517     }
518     return $output;
519 }
520
521 # Returns a list of aou objects in an array.
522 sub orgs {
523     my $self = shift;
524     unless ($self->{orgs} && @{$self->{orgs}}) {
525         my $fmClass = Fieldmapper::class_for_hint('aou');
526         my @classFields = $fmClass->real_fields();
527         my $classTable = $fmClass->Table();
528         my $query = 'select ' . join(',', @classFields);
529         $query .= "\nfrom $classTable";
530         my $sth = $self->{handle}->prepare($query);
531         if ($sth->execute()) {
532             my $result = $sth->fetchall_arrayref({});
533             my @orgs = map {$fmClass->from_bare_hash($_)} @{$result};
534             $self->{orgs} = \@orgs;
535         } else {
536             $self->{orgs} = [];
537         }
538     }
539     return @{$self->{orgs}};
540 }
541
542 # Returns an array of acpl objects.
543 sub shelves {
544     my $self = shift;
545
546     unless ($self->{shelves} && @{$self->{shelves}}) {
547         my $fmClass = Fieldmapper::class_for_hint('acpl');
548         my @classFields = $fmClass->real_fields();
549         my $classTable = $fmClass->Table();
550         my $query = 'select ' . join(',', @classFields);
551         $query .= "\nfrom $classTable";
552         my $result = $self->{handle}->selectall_arrayref($query, {Slice=>{}});
553         my @shelves = map {$fmClass->from_bare_hash($_)} @{$result};
554         $self->{shelves} = \@shelves;
555     }
556
557     return @{$self->{shelves}};
558 }
559
560 # Returns an array of acn objects for a given bre object or id.
561 sub acns_for_bre {
562     my $self = shift;
563     my $bre = shift;
564     $bre = $bre->id() if (ref($bre));
565
566     unless ($self->{acnHandle}) {
567         my $query = "select " . join(',', $self->{acnClass}->real_fields());
568         $query .= "\nfrom " . $self->{acnClass}->Table();
569         $query .= "\nwhere record = ?";
570         if (@{$self->{libs}}) {
571             $query .= "\nand owning_lib in (";
572             $query .= join(',', @{$self->{libs}}) . ")";
573         }
574         $query .= "\nand deleted = 'f'" unless($Marque::config->option_value('since'));
575         $self->{acnHandle} = $self->{handle}->prepare($query);
576     }
577
578     if ($self->{acnHandle}->execute($bre)) {
579         my $result = $self->{acnHandle}->fetchall_arrayref({});
580         return map {$self->{acnClass}->from_bare_hash($_)} @{$result};
581     }
582
583     # If for some reason, we don't find anything.
584     return undef;
585 }
586
587 # Returns an array of acp objects for a given bre object or id.
588 sub acps_for_bre {
589     my $self = shift;
590     my $bre = shift;
591     $bre = $bre->id() if (ref($bre));
592
593     my @orgs = $self->orgs();
594     my @locations = $self->shelves();
595
596     my @acns = $self->acns_for_bre($bre);
597     if (@acns) {
598         my $query = 'select ' . join(',', $self->{acpClass}->real_fields());
599         $query .= "\nfrom " . $self->{acpClass}->Table();
600         $query .= "\nwhere call_number in (";
601         $query .= join(',', map {$_->id()} @acns);
602         $query .= ")";
603         $query .= "\nand deleted = 'f'" unless ($Marque::config->option_value('since'));
604         my $result = $self->{handle}->selectall_arrayref($query, {Slice=>{}});
605         if ($result && @{$result}) {
606             my @acps = map {$self->{acpClass}->from_bare_hash($_)} @{$result};
607             foreach (@acps) {
608                 my $cn = $_->call_number();
609                 my $clib = $_->circ_lib();
610                 my $loc = $_->location();
611                 my ($org) = grep {$_->id() == $clib} @orgs;
612                 my ($acn) = grep {$_->id() == $cn} @acns;
613                 my ($location) = grep {$_->id() == $loc} @locations;
614                 my $olib = $acn->owning_lib();
615                 my ($owner) = grep {$_->id() == $olib} @orgs;
616                 $acn->owning_lib($owner);
617                 $_->call_number($acn);
618                 $_->circ_lib($org);
619                 $_->location($location);
620             }
621             return @acps;
622         }
623     }
624
625     # If for some reason, we don't find anything.
626     return undef;
627 }
628
629 # Retreive an array for sre objects when the --mfhd option is used.
630 sub sres_for_bre {
631     my $self = shift;
632     my $bre = shift;
633     $bre = $bre->id() if (ref($bre));
634     my @sres;
635     # Build a query to retrieve SREs when the MFHD option is passed.
636     if ($Marque::config->option_value('mfhd')) {
637         # Create a persistent handle as needed.
638         unless ($self->{sreSth}) {
639             my $query = "select " . join(',', $self->{sreClass}->real_fields());
640             $query .= "\nfrom " . $self->{sreClass}->Table();
641             $query .= "\nwhere record = ?";
642             $query .= "\nand deleted = 'f'" unless ($Marque::config->option_value('since'));
643             $self->{sreSth} = $self->{handle}->prepare($query);
644         }
645         if ($self->{sreSth}->execute($bre)) {
646             while (my $data = $self->{sreSth}->fetchrow_hashref) {
647                 push @sres, $self->{sreClass}->from_bare_hash($data);
648             }
649             $self->{sreSth}->finish; # Sometimes DBI complains.
650         }
651     }
652     # May be empty.
653     return @sres;
654 }
655
656 # Get authority records from the database.
657 package Marque::Authority;
658
659 sub new {
660     my $class = shift;
661     my $idlist = shift;
662     my $self = {idlist => $idlist};
663     $self->{handle} = Marque::Connector::connect(
664         $Marque::config->database_settings);
665     $self->{fmClass} = Fieldmapper::class_for_hint('are');
666     $self->{since_date} = Date::Manip::Date->new;
667     $self->{since_date}->parse($Marque::config->option_value('since'));
668     bless $self, $class;
669     return $self;
670 }
671
672 sub build_query {
673     my $self = shift;
674
675     # Get the information for are object from the Fieldmapper:
676     my @fields  = $self->{fmClass}->real_fields();
677     my $table = $self->{fmClass}->Table();
678
679     # Build the actual query.
680     my $select = "select " . join(',', @fields);
681     my $from = "from $table";
682     my $where = 'where ';
683
684     # If we have an idlist, we pretty much ignore anything else.
685     if ($self->{idlist} && @{$self->{idlist}}) {
686         $where .= 'id in (' . join(',', @{$self->{idlist}}) . ')';
687     } elsif ($Marque::config->option_value('since')) {
688         my $since_str = Marque::Connector::db_date($self->{since_date});
689         $where .= "edit_date > '$since_str'";
690         $where .= " or create_date > '$since_str'";
691     } else {
692         # We want all non-deleted records.
693         $where .= "deleted = 'f'";
694     }
695
696     $self->{query} = $select . "\n" . $from . "\n" . $where;
697 }
698
699 sub execute_query {
700     my $self = shift;
701     $self->build_query() unless ($self->{query});
702     $self->{sth} = $self->{handle}->prepare($self->{query});
703     return $self->{sth}->execute;
704 }
705
706 sub next {
707     my $self = shift;
708     my $output;
709     my $data = $self->{sth}->fetchrow_hashref;
710
711     if ($data) {
712         my $format = $Marque::config->option_value('format');
713         my $are = $self->{fmClass}->from_bare_hash($data);
714         if ($format eq 'ARE') {
715             $output = OpenSRF::Utils::JSON->perl2JSON($are);
716         } else {
717             my $r;
718             eval {
719                 $r = MARC::Record->new_from_xml($are->marc(),
720                                                 $Marque::config->option_value('encoding'),
721                                                 $Marque::config->option_value('format'));
722             };
723             if ($@) {
724                 print STDERR "Error in authority record " . $are->id() . "\n";
725                 print STDERR "$@\n";
726                 import MARC::File::XML; # Reset SAX Parser.
727                 return $self->next();
728             }
729             if ($Marque::config->option_value('replace_001')) {
730                 my $tcn = $r->field('001');
731                 if ($tcn) {
732                     $tcn->update($are->id());
733                 } else {
734                     $tcn = MARC::Field->new('001', $are->id());
735                     $r->insert_fields_ordered($tcn);
736                 }
737             }
738             if ($Marque::config->option_value('since')) {
739                 my $leader = $r->leader();
740                 if ($U->is_true($are->deleted())) {
741                     substr($leader, 5, 1) = 'd';
742                     $r->leader($leader);
743                 } else {
744                     my $create_date = Date::Manip::Date->new;
745                     $create_date->parse($are->create_date());
746                     my $edit_date = Date::Manip::Date->new;
747                     $edit_date->parse($are->edit_date());
748                     if ($self->{since_date}->cmp($create_date) < 0) {
749                         substr($leader, 5, 1) = 'n';
750                         $r->leader($leader);
751                     } elsif ($self->{since_date}->cmp($edit_date) < 0) {
752                         substr($leader, 5, 1) = 'c';
753                         $r->leader($leader);
754                     }
755                 }
756             }
757             if ($Marque::config->option_value('format') eq 'XML') {
758                 $output = $r->as_xml_record;
759                 $output =~ s/^<\?.+?\?>$//mo;
760             } else {
761                 $output = $r->as_usmarc;
762             }
763         }
764     }
765
766     return $output;
767 }
768
769 # ------------------------------------------------------------------
770 # Since the ultimate output is largely independent of the type of the
771 # records, we use a single subpackage to group our output routines.
772 package Marque::Output;
773
774 sub output {
775     my $extractor = shift;
776     if ($extractor->execute_query) {
777         if ($Marque::config->option_value('encoding') eq 'UTF-8') {
778             binmode(STDOUT, ':utf8');
779         } else {
780             binmode(STDOUT, ':raw');
781         }
782
783         &preamble;
784         while (my $output = $extractor->next()) {
785             print $output;
786         }
787         &postamble;
788     } else {
789         print STDERR $extractor->{query} if ($Marque::config->option_value('debug'));
790         die "Database query failed!";
791     }
792 }
793
794 sub preamble {
795     if ($Marque::config->option_value('format') eq 'XML') {
796         my $encoding = $Marque::config->option_value('encoding');
797         print <<PREAMBLE;
798 <?xml version="1.0" encoding="$encoding"?>
799 <collection xmlns='http://www.loc.gov/MARC21/slim'>
800 PREAMBLE
801     }
802 }
803
804 sub postamble {
805     if ($Marque::config->option_value('format') eq 'XML') {
806         print "</collection>\n";
807     }
808 }
809
810 1;