2 # ---------------------------------------------------------------
3 # Copyright © 2013 Merrimack Valley Library Consortium
4 # Jason Stephenson <jstephenson@mvlc.org>
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.
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 # ---------------------------------------------------------------
18 use OpenILS::Utils::Fieldmapper;
19 use OpenILS::Application::AppUtils;
20 use OpenSRF::Utils::JSON;
23 use MARC::File::XML (BinaryEncoding => 'UTF-8');
24 use Date::Manip::Date;
26 my $U = 'OpenILS::Application::AppUtils';
28 binmode(STDERR, ':utf8');
32 our $config = Marque::Config->new();
33 Fieldmapper->import(IDL => $config->option_value('xml-idl'));
35 # Look for passed in ids:
37 if ($config->need_ids()) {
38 print STDERR "Waiting for input\n" if (-t);
40 push @ids, $i if ($i =~ /^\s*[0-9]+\s*$/);
45 if ($config->option_value('type') eq 'authority') {
46 $exporter = Marque::Authority->new(\@ids);
48 $exporter = Marque::Biblio->new(\@ids);
51 Marque::Output::output($exporter);
53 # ------------------------------------------------------------------
54 package Marque::Config;
57 use List::MoreUtils qw(none);
59 use OpenSRF::Utils::SettingsClient;
61 use constant FORMATS => qw(USMARC UNIMARC XML BRE ARE);
62 use constant STORES => qw(reporter cstore storage);
63 use constant TYPES => qw(authority biblio);
71 # For command line options.
74 # set some default values
75 $opts{'format'} = 'USMARC';
76 $opts{'encoding'} = 'MARC8';
77 $opts{'type'} = 'biblio';
80 $opts{'config'} = '@sysconfdir@/opensrf_core.xml';
81 $opts{'store'} = 'reporter';
104 This script exports MARC authority, bibliographic, and serial holdings
105 records from an Evergreen database.
107 Input to this script can consist of a list of record IDs, with one record ID
108 per line, corresponding to the record ID in the Evergreen database table of
109 your requested record type.
111 Alternately, passing the --all option will attempt to export all records of
112 the specified type from the Evergreen database. The --all option starts at
113 record ID 1 and increments the ID by 1 until the largest ID in the database
114 is retrieved. This may not be very efficient for databases with large gaps
115 in their ID sequences.
118 --help or -h This screen.
119 --config or -c Configuration file [@sysconfdir@/opensrf_core.xml]
120 --format or -f Output format (USMARC, UNIMARC, XML, BRE, ARE) [USMARC]
121 --encoding or -e Output encoding (UTF-8, ISO-8859-?, MARC8) [MARC8]
122 --xml-idl or -x Location of the IDL XML
123 --timeout Remains for backward compatibility. No longer used.
124 --type or -t Record type (BIBLIO, AUTHORITY) [BIBLIO]
125 --all or -a Export all records; ignores input list
126 --replace_001 Replace the 001 field value with the record ID
127 --store Use the given storage backend to connect to the database.
128 Choices are (reporter, cstore, storage) [reporter]
129 --since Export records modified since a certain date and time.
131 Additional options for type = 'BIBLIO':
132 --items or -i Include items (holdings) in the output
133 --money Currency symbol to use in item price field [\$]
134 --mfhd Export serial MFHD records for associated bib records
135 Not compatible with --format=BRE
136 --location or -l MARC Location Code for holdings from
137 http://www.loc.gov/marc/organizations/orgshome.html
138 --library Export the bibliographic records that have attached
139 holdings for the listed library or libraries as
140 identified by shortname
144 To export a set of USMARC records in a file named "output_file" based on the
145 IDs contained in a file named "list_of_ids":
146 cat list_of_ids | $0 > output_file
148 To export a set of MARC21XML authority records in a file named "output.xml"
149 for all authority records in the database:
150 $0 --format XML --type AUTHORITY --all > output.xml
152 To export a set of USMARC bibliographic records encoded in UTF-8 in a file
153 named "sys1_bibs.mrc" based on records which have attached callnumbers for the
154 libraries with the short names "BR1" and "BR2":
156 $0 --library BR1 --library BR2 --encoding UTF-8 > sys1_bibs.mrc
162 OpenSRF::System->bootstrap_client( config_file => $opts{config} );
163 my $sclient = OpenSRF::Utils::SettingsClient->new();
164 unless ($opts{'xml-idl'}) {
165 $opts{'xml-idl'} = $sclient->config_value('IDL');
168 # Validate some of the settings.
169 if ($opts{all} && $opts{library}) {
170 die('Incompatible arguments: you cannot combine a request for all ' .
171 'records with a request for records by library');
173 if ($opts{all} && $opts{since}) {
174 die('Incompatible arguments: you cannot combine a request for all ' .
175 'records with a request for records added or changed since a certain date');
177 $opts{type} = lc($opts{type});
178 if (none {$_ eq $opts{type}} (TYPES)) {
179 die "Please select a supported type. ".
180 "Right now that means one of [".
181 join('|',(FORMATS)). "]\n";
183 $opts{format} = uc($opts{format});
184 if (none {$_ eq $opts{format}} (FORMATS)) {
185 die "Please select a supported format. ".
186 "Right now that means one of [".
187 join('|',(FORMATS)). "]\n";
190 if ($opts{format} eq 'ARE' && $opts{type} ne 'authority') {
191 die "Format ARE is not compatible with type " . $opts{type};
193 if ($opts{format} eq 'BRE' && $opts{type} ne 'biblio') {
194 die "Format BRE is not compatible with type " . $opts{type};
196 if ($opts{format} eq 'BRE' && $opts{items}) {
197 die "Format BRE is not compatible with exporting holdings."
201 if ($opts{type} ne 'biblio') {
202 die "MFHD export only works with bibliographic records.";
203 } elsif ($opts{format} eq 'BRE') {
204 die "MFHD export incompatible with format BRE.";
208 $opts{store} = lc($opts{store});
209 if (none {$_ eq $opts{store}} (STORES)) {
210 die "Please select a supported store. ".
211 "Right now that means one of [".
212 join('|',(STORES)). "]\n";
215 if ($opts{store} eq 'reporter') {
216 $app = 'open-ils.reporter-store';
218 $app = 'open-ils.' . $opts{store};
220 if ($app eq 'open-ils.storage') {
221 $self->{dbsettings} = $sclient->config_value(
222 apps => $app => app_settings => databases => 'database');
224 $self->{dbsettings} = $sclient->config_value(
225 apps => $app => app_settings => 'database');
228 $opts{encoding} = uc($opts{encoding});
230 $self->{'options'} = \%opts;
236 my ($self, $option) = @_;
237 return $self->{options}->{$option};
240 sub database_settings {
242 return $self->{dbsettings};
249 $rv = 0 if ($self->{options}->{all});
250 $rv = 0 if ($self->{options}->{since});
251 $rv = 0 if ($self->{options}->{library});
256 # ------------------------------------------------------------------
257 # This package exists to get a connection to the database. Since
258 # we'll need one for both biblio records and authorities, we've made a
259 # single subpackage with a function so that we don't have to duplicate
261 package Marque::Connector;
265 # Pass a Marque::Config object's database_settings return value into
266 # this to get a DBI connection.
268 # my $db = Marque::Connector::connect($config->database_settings);
272 # Build a connect string from the args.
273 my $connect_string = 'DBI:Pg:';
274 $connect_string .= 'dbname=' . $args->{db} . ';';
275 $connect_string .= 'host=' . $args->{host} . ';';
276 $connect_string .= 'port=' . $args->{port};
278 my $db_handle = DBI->connect($connect_string,
279 $args->{user}, $args->{pw});
283 # A function to get the date into a format better for PostgreSQL.
287 if (ref($input) eq 'Date::Manip::Date') {
290 $date = Date::Manip::Date->new();
291 if ($date->parse($input)) {
292 die "Can't parse date $input";
295 return $date->printf("%Y-%m-%dT%H:%M:%S%z");
298 # ------------------------------------------------------------------
299 # You would typically have the next two packages inherit from a common
300 # superclass, but ineritance doesn't seem to work when all packages
301 # are in single file, so we have some duplicated code between these
304 # Get bibliographic records from the database.
305 package Marque::Biblio;
310 my $self = {idlist => $idlist};
311 $self->{handle} = Marque::Connector::connect(
312 $Marque::config->database_settings);
313 $self->{since_date} = Date::Manip::Date->new;
314 $self->{since_date}->parse($Marque::config->option_value('since'));
316 # We need multiple fieldmapper classes depending on our
317 # options. We'll just get the information that we'll need for them
318 # all right here instead of only fetching the information when
320 $self->{breClass} = Fieldmapper::class_for_hint('bre');
321 $self->{acnClass} = Fieldmapper::class_for_hint('acn');
322 $self->{acpClass} = Fieldmapper::class_for_hint('acp');
323 $self->{sreClass} = Fieldmapper::class_for_hint('sre');
325 # Make an arrayref of shortname ids if the library option was
328 if ($Marque::config->option_value('library')) {
329 # This is done not only for speed, but to prevent SQL injection.
330 my $sth = $self->{handle}->prepare('select id from actor.org_unit where shortname=any(?::text[])');
331 if ($sth->execute($Marque::config->option_value('library'))) {
332 my $r = $sth->fetchall_arrayref();
333 my @ids = map {$_->[0]} @{$r};
334 $self->{libs} = \@ids;
346 # Get the field names and tables for our classes. We add the fully
347 # qualified table names to the fields so that the joins will work.
348 my $breTable = $self->{breClass}->Table();
349 my @breFields = map {$breTable . '.' . $_} $self->{breClass}->real_fields();
350 my $acnTable = $self->{acnClass}->Table();
351 my $acpTable = $self->{acpClass}->Table();
353 # Now we build the query in pieces:
355 # We always select the bre fields:
356 my $select = 'select distinct ' . join(',', @breFields);
357 # We always use the bre table.
358 my $from = "from $breTable";
360 # If have the libraries or items options, we need to join the
361 # asset.call_number table. If we have both, this variable checks
362 # that it has already been joined so we don't create an invalid
365 # Join to the acn table as needed for the library option.
366 if (@{$self->{libs}}) {
370 join $acnTable on $acnTable.record = $breTable.id
371 and $acnTable.owning_lib in (
373 $from .= join(',', @{$self->{libs}}) . ")";
374 $from .= "\nand $acnTable.deleted = 'f'" unless ($Marque::config->option_value('since'));
377 if ($Marque::config->option_value('items')) {
378 unless ($acn_joined) {
379 $from .= "\njoin $acnTable on $acnTable.record = $breTable.id";
380 $from .= "\nand $acnTable.deleted = 'f'" unless ($Marque::config->option_value('since'));
382 $from .= "\njoin $acpTable on $acpTable.call_number = $acnTable.id";
383 $from .= "\nand $acpTable.deleted = 'f'" unless ($Marque::config->option_value('since'));
386 # The where really depends on a few options:
387 my $where = "where $breTable.id > 0 and ";
388 # We fill in the where as necessary.
389 if ($self->{idlist} && @{$self->{idlist}}) {
390 $where .= "$breTable.id in (" . join(',', @{$self->{idlist}}) . ')';
391 } elsif ($Marque::config->option_value('since')) {
392 my $since_str = Marque::Connector::db_date($self->{since_date});
393 $where .= "($breTable.edit_date > '$since_str'";
394 $where .= " or $breTable.create_date > '$since_str')";
396 # We want all non-deleted records.
397 $where .= "$breTable.deleted = 'f'";
400 $self->{query} = $select . "\n" . $from . "\n" . $where;
405 $self->build_query() unless ($self->{query});
406 $self->{sth} = $self->{handle}->prepare($self->{query});
407 return $self->{sth}->execute;
414 # $r holds the record object, either sre or bre. $marc holds the
415 # current record's MARC, either sre.marc or bre.marc
417 # If we have the mfhd option and we've previously retrieved some
418 # sres, then we output one of the retrieved sres for each call
419 # until we run out. These sres "go with" the previous bib record.
420 if ($Marque::config->option_value('mfhd') && $self->{mfhds} && @{$self->{mfhds}}) {
421 $r = shift(@{$self->{mfhds}});
423 local $SIG{__WARN__} = sub {
424 my $message = "Warning from serial record " . $r->id() . ": "
428 $marc = MARC::Record->new_from_xml($r->marc(),
429 $Marque::config->option_value('encoding'),
430 $Marque::config->option_value('format'));
433 warn("Error in serial record " . $r->id() . ": $@");
434 import MARC::File::XML; # Reset SAX Parser.
435 return $self->next();
438 my $data = $self->{sth}->fetchrow_hashref;
440 $r = $self->{breClass}->from_bare_hash($data);
441 if ($Marque::config->option_value('format') eq 'BRE') {
442 $output = OpenSRF::Utils::JSON->perl2JSON($r);
445 local $SIG{__WARN__} = sub {
446 my $message = "Warning from bibliographic record " . $r->id() . ": "
450 $marc = MARC::Record->new_from_xml($r->marc(),
451 $Marque::config->option_value('encoding'),
452 $Marque::config->option_value('format'));
455 warn("Error in bibliographic record " . $r->id() . ": $@");
456 import MARC::File::XML; # Reset SAX Parser.
457 return $self->next();
459 if ($Marque::config->option_value('replace_001')) {
460 my $tcn = $marc->field('001');
462 $tcn->update($r->id());
464 $tcn = MARC::Field->new('001', $r->id());
465 $marc->insert_fields_ordered($tcn);
468 if ($Marque::config->option_value('items')) {
469 my @acps = $self->acps_for_bre($r);
470 foreach my $acp (@acps) {
472 my $location = $Marque::config->option_value('location');
473 my $price = ($acp->price() ? $Marque::config->option_value('money').$acp->price() : '');
475 local $SIG{__WARN__} = sub {
476 my $message = "Warning from bibliographic record " . $r->id() . ": "
480 $marc->insert_grouped_field(
483 ($location ? ('a' => $location) : ()),
484 b => Encode::decode_utf8($acp->call_number()->owning_lib()->shortname()),
485 b => Encode::decode_utf8($acp->circ_lib()->shortname()),
486 c => Encode::decode_utf8($acp->location()->name()),
487 j => Encode::decode_utf8($acp->call_number()->label()),
488 ($acp->circ_modifier() ? (g => Encode::decode_utf8($acp->circ_modifier())) : ()),
489 p => Encode::decode_utf8($acp->barcode()),
490 ($price ? (y => Encode::decode_utf8($price)) : ()),
491 ($acp->copy_number() ? (t => $acp->copy_number()) : ()),
492 ($U->is_true($acp->ref()) ? (x => 'reference') : ()),
493 (!$U->is_true($acp->holdable()) ? (x => 'unholdable') : ()),
494 (!$U->is_true($acp->circulate()) ? (x => 'noncirculating') : ()),
495 (!$U->is_true($acp->opac_visible()) ? (x => 'hidden') : ())
499 warn("Error in bibliographic record " . $r->id() . ": $@");
500 import MARC::File::XML; # Reset SAX Parser.
501 return $self->next();
505 if ($Marque::config->option_value('mfhd')) {
506 $self->{mfhds} = [$self->sres_for_bre($r)];
511 # Common stuff that doesn't depend on record type.
513 if ($Marque::config->option_value('since')) {
514 my $leader = $marc->leader();
515 if ($U->is_true($r->deleted())) {
516 substr($leader, 5, 1) = 'd';
517 $marc->leader($leader);
519 my $create_date = Date::Manip::Date->new;
520 $create_date->parse($r->create_date());
521 my $edit_date = Date::Manip::Date->new;
522 $edit_date->parse($r->edit_date());
523 if ($self->{since_date}->cmp($create_date) < 0) {
524 substr($leader, 5, 1) = 'n';
525 $marc->leader($leader);
526 } elsif ($self->{since_date}->cmp($edit_date) < 0) {
527 substr($leader, 5, 1) = 'c';
528 $marc->leader($leader);
532 if ($Marque::config->option_value('format') eq 'XML') {
534 local $SIG{__WARN__} = sub {
535 my $message = "Warning from bibliographic record " . $r->id() . ": "
539 $output = $marc->as_xml_record;
540 $output =~ s/^<\?.+?\?>$//mo;
543 warn("Error in bibliographic record " . $r->id() . ": $@");
544 return $self->next();
548 local $SIG{__WARN__} = sub {
549 my $message = "Warning from bibliographic record " . $r->id() . ": "
553 $output = $marc->as_usmarc;
556 warn("Error in bibliographic record " . $r->id() . ": $@");
557 return $self->next();
564 # Returns a list of aou objects in an array.
567 unless ($self->{orgs} && @{$self->{orgs}}) {
568 my $fmClass = Fieldmapper::class_for_hint('aou');
569 my @classFields = $fmClass->real_fields();
570 my $classTable = $fmClass->Table();
571 my $query = 'select ' . join(',', @classFields);
572 $query .= "\nfrom $classTable";
573 my $sth = $self->{handle}->prepare($query);
574 if ($sth->execute()) {
575 my $result = $sth->fetchall_arrayref({});
576 my @orgs = map {$fmClass->from_bare_hash($_)} @{$result};
577 $self->{orgs} = \@orgs;
582 return @{$self->{orgs}};
585 # Returns an array of acpl objects.
589 unless ($self->{shelves} && @{$self->{shelves}}) {
590 my $fmClass = Fieldmapper::class_for_hint('acpl');
591 my @classFields = $fmClass->real_fields();
592 my $classTable = $fmClass->Table();
593 my $query = 'select ' . join(',', @classFields);
594 $query .= "\nfrom $classTable";
595 my $result = $self->{handle}->selectall_arrayref($query, {Slice=>{}});
596 my @shelves = map {$fmClass->from_bare_hash($_)} @{$result};
597 $self->{shelves} = \@shelves;
600 return @{$self->{shelves}};
603 # Returns an array of acn objects for a given bre object or id.
607 $bre = $bre->id() if (ref($bre));
609 unless ($self->{acnHandle}) {
610 my $query = "select " . join(',', $self->{acnClass}->real_fields());
611 $query .= "\nfrom " . $self->{acnClass}->Table();
612 $query .= "\nwhere record = ?";
613 if (@{$self->{libs}}) {
614 $query .= "\nand owning_lib in (";
615 $query .= join(',', @{$self->{libs}}) . ")";
617 $query .= "\nand deleted = 'f'" unless($Marque::config->option_value('since'));
618 $self->{acnHandle} = $self->{handle}->prepare($query);
621 if ($self->{acnHandle}->execute($bre)) {
622 my $result = $self->{acnHandle}->fetchall_arrayref({});
623 return map {$self->{acnClass}->from_bare_hash($_)} @{$result};
626 # If for some reason, we don't find anything.
630 # Returns an array of acp objects for a given bre object or id.
634 $bre = $bre->id() if (ref($bre));
636 my @orgs = $self->orgs();
637 my @locations = $self->shelves();
639 my @acns = $self->acns_for_bre($bre);
641 my $query = 'select ' . join(',', $self->{acpClass}->real_fields());
642 $query .= "\nfrom " . $self->{acpClass}->Table();
643 $query .= "\nwhere call_number in (";
644 $query .= join(',', map {$_->id()} @acns);
646 $query .= "\nand deleted = 'f'" unless ($Marque::config->option_value('since'));
647 my $result = $self->{handle}->selectall_arrayref($query, {Slice=>{}});
648 if ($result && @{$result}) {
649 my @acps = map {$self->{acpClass}->from_bare_hash($_)} @{$result};
651 my $cn = $_->call_number();
652 my $clib = $_->circ_lib();
653 my $loc = $_->location();
654 my ($org) = grep {$_->id() == $clib} @orgs;
655 my ($acn) = grep {$_->id() == $cn} @acns;
656 my ($location) = grep {$_->id() == $loc} @locations;
657 my $olib = $acn->owning_lib();
658 my ($owner) = grep {$_->id() == $olib} @orgs;
659 $acn->owning_lib($owner);
660 $_->call_number($acn);
662 $_->location($location);
668 # If for some reason, we don't find anything.
672 # Retreive an array for sre objects when the --mfhd option is used.
676 $bre = $bre->id() if (ref($bre));
678 # Build a query to retrieve SREs when the MFHD option is passed.
679 if ($Marque::config->option_value('mfhd')) {
680 # Create a persistent handle as needed.
681 unless ($self->{sreSth}) {
682 my $query = "select " . join(',', $self->{sreClass}->real_fields());
683 $query .= "\nfrom " . $self->{sreClass}->Table();
684 $query .= "\nwhere record = ?";
685 $query .= "\nand deleted = 'f'" unless ($Marque::config->option_value('since'));
686 $self->{sreSth} = $self->{handle}->prepare($query);
688 if ($self->{sreSth}->execute($bre)) {
689 while (my $data = $self->{sreSth}->fetchrow_hashref) {
690 push @sres, $self->{sreClass}->from_bare_hash($data);
692 $self->{sreSth}->finish; # Sometimes DBI complains.
699 # Get authority records from the database.
700 package Marque::Authority;
705 my $self = {idlist => $idlist};
706 $self->{handle} = Marque::Connector::connect(
707 $Marque::config->database_settings);
708 $self->{fmClass} = Fieldmapper::class_for_hint('are');
709 $self->{since_date} = Date::Manip::Date->new;
710 $self->{since_date}->parse($Marque::config->option_value('since'));
718 # Get the information for are object from the Fieldmapper:
719 my @fields = $self->{fmClass}->real_fields();
720 my $table = $self->{fmClass}->Table();
722 # Build the actual query.
723 my $select = "select " . join(',', @fields);
724 my $from = "from $table";
725 my $where = 'where ';
727 # If we have an idlist, we pretty much ignore anything else.
728 if ($self->{idlist} && @{$self->{idlist}}) {
729 $where .= 'id in (' . join(',', @{$self->{idlist}}) . ')';
730 } elsif ($Marque::config->option_value('since')) {
731 my $since_str = Marque::Connector::db_date($self->{since_date});
732 $where .= "edit_date > '$since_str'";
733 $where .= " or create_date > '$since_str'";
735 # We want all non-deleted records.
736 $where .= "deleted = 'f'";
739 $self->{query} = $select . "\n" . $from . "\n" . $where;
744 $self->build_query() unless ($self->{query});
745 $self->{sth} = $self->{handle}->prepare($self->{query});
746 return $self->{sth}->execute;
752 my $data = $self->{sth}->fetchrow_hashref;
755 my $format = $Marque::config->option_value('format');
756 my $r = $self->{fmClass}->from_bare_hash($data);
757 if ($format eq 'ARE') {
758 $output = OpenSRF::Utils::JSON->perl2JSON($r);
762 local $SIG{__WARN__} = sub {
763 my $message = "Warning from authority record " . $r->id() . ": "
767 $marc = MARC::Record->new_from_xml($r->marc(),
768 $Marque::config->option_value('encoding'),
769 $Marque::config->option_value('format'));
772 warn("Error in authority record " . $r->id() . ": $@");
773 import MARC::File::XML; # Reset SAX Parser.
774 return $self->next();
776 if ($Marque::config->option_value('replace_001')) {
777 my $tcn = $marc->field('001');
779 $tcn->update($r->id());
781 $tcn = MARC::Field->new('001', $r->id());
782 $marc->insert_fields_ordered($tcn);
785 if ($Marque::config->option_value('since')) {
786 my $leader = $marc->leader();
787 if ($U->is_true($r->deleted())) {
788 substr($leader, 5, 1) = 'd';
789 $marc->leader($leader);
791 my $create_date = Date::Manip::Date->new;
792 $create_date->parse($r->create_date());
793 my $edit_date = Date::Manip::Date->new;
794 $edit_date->parse($r->edit_date());
795 if ($self->{since_date}->cmp($create_date) < 0) {
796 substr($leader, 5, 1) = 'n';
797 $marc->leader($leader);
798 } elsif ($self->{since_date}->cmp($edit_date) < 0) {
799 substr($leader, 5, 1) = 'c';
800 $marc->leader($leader);
804 if ($Marque::config->option_value('format') eq 'XML') {
806 local $SIG{__WARN__} = sub {
807 my $message = "Warning from authority record " . $r->id() . ": "
811 $output = $marc->as_xml_record;
812 $output =~ s/^<\?.+?\?>$//mo;
815 warn("Error in authority record " . $r->id() . ": $@");
816 return $self->next();
820 local $SIG{__WARN__} = sub {
821 my $message = "Warning from authority record " . $r->id() . ": "
825 $output = $marc->as_usmarc;
828 warn("Error in authority record " . $r->id() . ": $@");
829 return $self->next();
838 # ------------------------------------------------------------------
839 # Since the ultimate output is largely independent of the type of the
840 # records, we use a single subpackage to group our output routines.
841 package Marque::Output;
844 my $extractor = shift;
845 if ($extractor->execute_query) {
846 if ($Marque::config->option_value('encoding') eq 'UTF-8') {
847 binmode(STDOUT, ':utf8');
849 binmode(STDOUT, ':raw');
853 while (my $output = $extractor->next()) {
858 print STDERR $extractor->{query} if ($Marque::config->option_value('debug'));
859 die "Database query failed!";
864 if ($Marque::config->option_value('format') eq 'XML') {
865 my $encoding = $Marque::config->option_value('encoding');
867 <?xml version="1.0" encoding="$encoding"?>
868 <collection xmlns='http://www.loc.gov/MARC21/slim'>
874 if ($Marque::config->option_value('format') eq 'XML') {
875 print "</collection>\n";