File Coverage

blib/lib/MARC/File/XML.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package MARC::File::XML;
2              
3 10     10   70952 use warnings;
  10         25  
  10         369  
4 10     10   58 use strict;
  10         23  
  10         246  
5 10     10   53 use vars qw( $VERSION %_load_args );
  10         22  
  10         564  
6 10     10   61 use base qw( MARC::File );
  10         20  
  10         394062  
7 10     10   159747 use MARC::Record;
  10         441187  
  10         411  
8 10     10   73 use MARC::Field;
  10         51  
  10         168  
9 10     10   5768 use XML::LibXML;
  0            
  0            
10              
11             use MARC::Charset qw( marc8_to_utf8 utf8_to_marc8 );
12             use IO::File;
13             use Carp qw( croak );
14             use Encode ();
15              
16             $VERSION = '1.0.5';
17              
18             our $parser;
19              
20             sub import {
21             my $class = shift;
22             %_load_args = @_;
23             $_load_args{ DefaultEncoding } ||= 'UTF-8';
24             $_load_args{ RecordFormat } ||= 'USMARC';
25             }
26              
27             =head1 NAME
28              
29             MARC::File::XML - Work with MARC data encoded as XML
30              
31             =head1 SYNOPSIS
32              
33             ## Loading with USE options
34             use MARC::File::XML ( BinaryEncoding => 'utf8', RecordFormat => 'UNIMARC' );
35              
36             ## Setting the record format without USE options
37             MARC::File::XML->default_record_format('USMARC');
38            
39             ## reading with MARC::Batch
40             my $batch = MARC::Batch->new( 'XML', $filename );
41             my $record = $batch->next();
42              
43             ## or reading with MARC::File::XML explicitly
44             my $file = MARC::File::XML->in( $filename );
45             my $record = $file->next();
46              
47             ## serialize a single MARC::Record object as XML
48             print $record->as_xml();
49              
50             ## write a bunch of records to a file
51             my $file = MARC::File::XML->out( 'myfile.xml' );
52             $file->write( $record1 );
53             $file->write( $record2 );
54             $file->write( $record3 );
55             $file->close();
56              
57             ## instead of writing to disk, get the xml directly
58             my $xml = join( "\n",
59             MARC::File::XML::header(),
60             MARC::File::XML::record( $record1 ),
61             MARC::File::XML::record( $record2 ),
62             MARC::File::XML::footer()
63             );
64              
65             =head1 DESCRIPTION
66              
67             The MARC-XML distribution is an extension to the MARC-Record distribution for
68             working with MARC21 data that is encoded as XML. The XML encoding used is the
69             MARC21slim schema supplied by the Library of Congress. More information may
70             be obtained here: http://www.loc.gov/standards/marcxml/
71              
72             You must have MARC::Record installed to use MARC::File::XML. In fact
73             once you install the MARC-XML distribution you will most likely not use it
74             directly, but will have an additional file format available to you when you
75             use MARC::Batch.
76              
77             This version of MARC-XML supersedes an the versions ending with 0.25 which
78             were used with the MARC.pm framework. MARC-XML now uses MARC::Record
79             exclusively.
80              
81             If you have any questions or would like to contribute to this module please
82             sign on to the perl4lib list. More information about perl4lib is available
83             at L.
84              
85             =head1 METHODS
86              
87             When you use MARC::File::XML your MARC::Record objects will have two new
88             additional methods available to them:
89              
90             =head2 MARC::File::XML->default_record_format([$format])
91              
92             Sets or returns the default record format used by MARC::File::XML. Valid
93             formats are B, B, B and B.
94              
95             MARC::File::XML->default_record_format('UNIMARC');
96              
97             =cut
98              
99             sub default_record_format {
100             my $self = shift;
101             my $format = shift;
102              
103             $_load_args{RecordFormat} = $format if ($format);
104              
105             return $_load_args{RecordFormat};
106             }
107              
108              
109             =head2 as_xml()
110              
111             Returns a MARC::Record object serialized in XML. You can pass an optional format
112             parameter to tell MARC::File::XML what type of record (USMARC, UNIMARC, UNIMARCAUTH) you are
113             serializing.
114              
115             print $record->as_xml([$format]);
116              
117             =cut
118              
119             sub MARC::Record::as_xml {
120             my $record = shift;
121             my $format = shift || $_load_args{RecordFormat};
122             return( MARC::File::XML::encode( $record, $format ) );
123             }
124              
125             =head2 as_xml_record([$format])
126              
127             Returns a MARC::Record object serialized in XML without a collection wrapper.
128             You can pass an optional format parameter to tell MARC::File::XML what type of
129             record (USMARC, UNIMARC, UNIMARCAUTH) you are serializing.
130              
131             print $record->as_xml_record('UNIMARC');
132              
133             =cut
134              
135             sub MARC::Record::as_xml_record {
136             my $record = shift;
137             my $format = shift || $_load_args{RecordFormat};
138             return( MARC::File::XML::encode( $record, $format, 1 ) );
139             }
140              
141             =head2 new_from_xml([$encoding, $format])
142              
143             If you have a chunk of XML and you want a record object for it you can use
144             this method to generate a MARC::Record object. You can pass an optional
145             encoding parameter to specify which encoding (UTF-8 or MARC-8) you would like
146             the resulting record to be in. You can also pass a format parameter to specify
147             the source record type, such as UNIMARC, UNIMARCAUTH, USMARC or MARC21.
148              
149             my $record = MARC::Record->new_from_xml( $xml, $encoding, $format );
150              
151             Note: only works for single record XML chunks.
152              
153             =cut
154              
155             sub MARC::Record::new_from_xml {
156             my $xml = shift;
157             ## to allow calling as MARC::Record::new_from_xml()
158             ## or MARC::Record->new_from_xml()
159             $xml = shift if ( ref($xml) || ($xml eq "MARC::Record") );
160              
161             my $enc = shift || $_load_args{BinaryEncoding};
162             my $format = shift || $_load_args{RecordFormat};
163             return( MARC::File::XML::decode( $xml, $enc, $format ) );
164             }
165              
166             =pod
167              
168             If you want to write records as XML to a file you can use out() with write()
169             to serialize more than one record as XML.
170              
171             =head2 out()
172              
173             A constructor for creating a MARC::File::XML object that can write XML to a
174             file. You must pass in the name of a file to write XML to. If the $encoding
175             parameter or the DefaultEncoding (see above) is set to UTF-8 then the binmode
176             of the output file will be set appropriately.
177              
178             my $file = MARC::File::XML->out( $filename [, $encoding] );
179              
180             =cut
181              
182             sub out {
183             my ( $class, $filename, $enc ) = @_;
184             my $fh = IO::File->new( ">$filename" ) or croak( $! );
185             $enc ||= $_load_args{DefaultEncoding};
186              
187             if ($enc =~ /^utf-?8$/oi) {
188             $fh->binmode(':utf8');
189             } else {
190             $fh->binmode(':raw');
191             }
192              
193             my %self = (
194             filename => $filename,
195             fh => $fh,
196             header => 0,
197             encoding => $enc
198             );
199             return( bless \%self, ref( $class ) || $class );
200             }
201              
202             =head2 write()
203              
204             Used in tandem with out() to write records to a file.
205              
206             my $file = MARC::File::XML->out( $filename );
207             $file->write( $record1 );
208             $file->write( $record2 );
209              
210             =cut
211              
212             sub write {
213             my ( $self, $record, $enc ) = @_;
214             if ( ! $self->{ fh } ) {
215             croak( "MARC::File::XML object not open for writing" );
216             }
217             if ( ! $record ) {
218             croak( "must pass write() a MARC::Record object" );
219             }
220             ## print the XML header if we haven't already
221             if ( ! $self->{ header } ) {
222             $enc ||= $self->{ encoding } || $_load_args{DefaultEncoding};
223             $self->{ fh }->print( header( $enc ) );
224             $self->{ header } = 1;
225             }
226             ## print out the record
227             $self->{ fh }->print( record( $record ) ) || croak( $! );
228             return( 1 );
229             }
230              
231             =head2 close()
232              
233             When writing records to disk the filehandle is automatically closed when you
234             the MARC::File::XML object goes out of scope. If you want to close it explicitly
235             use the close() method.
236              
237             =cut
238              
239             sub close {
240             my $self = shift;
241             if ( $self->{ fh } ) {
242             $self->{ fh }->print( footer() ) if $self->{ header };
243             $self->{ fh } = undef;
244             $self->{ filename } = undef;
245             $self->{ header } = undef;
246             }
247             return( 1 );
248             }
249              
250             ## makes sure that the XML file is closed off
251              
252             sub DESTROY {
253             shift->close();
254             }
255              
256             =pod
257              
258             If you want to generate batches of records as XML, but don't want to write to
259             disk you'll have to use header(), record() and footer() to generate the
260             different portions.
261              
262             $xml = join( "\n",
263             MARC::File::XML::header(),
264             MARC::File::XML::record( $record1 ),
265             MARC::File::XML::record( $record2 ),
266             MARC::File::XML::record( $record3 ),
267             MARC::File::XML::footer()
268             );
269              
270             =head2 header()
271              
272             Returns a string of XML to use as the header to your XML file.
273              
274             =cut
275              
276             sub header {
277             my $enc = shift;
278             $enc = shift if ( $enc && (ref($enc) || ($enc eq "MARC::File::XML")) );
279             $enc ||= 'UTF-8';
280             return( <
281            
282            
283             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
284             xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
285             xmlns="http://www.loc.gov/MARC21/slim">
286             MARC_XML_HEADER
287             }
288              
289             =head2 footer()
290              
291             Returns a string of XML to use at the end of your XML file.
292              
293             =cut
294              
295             sub footer {
296             return( "" );
297             }
298              
299             =head2 record()
300              
301             Returns a chunk of XML suitable for placement between the header and the footer.
302              
303             =cut
304              
305             sub record {
306             my $record = shift;
307             my $format = shift;
308             my $include_full_record_header = shift;
309             my $enc = shift;
310              
311             $format ||= $_load_args{RecordFormat};
312              
313             my $_transcode = 0;
314             my $ldr = $record->leader;
315             my $original_encoding = substr($ldr,9,1);
316              
317             # Does the record think it is already Unicode?
318             if ($original_encoding ne 'a' && lc($format) !~ /^unimarc/o) {
319             # If not, we'll make it so
320             $_transcode++;
321             substr($ldr,9,1,'a');
322             $record->leader( $ldr );
323             }
324              
325             my @xml = ();
326              
327             if ($include_full_record_header) {
328             push @xml, <
329            
330            
331             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
332             xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
333             xmlns="http://www.loc.gov/MARC21/slim">
334             HEADER
335              
336             } else {
337             push( @xml, "" );
338             }
339              
340             push( @xml, " " . escape( $record->leader ) . "" );
341              
342             foreach my $field ( $record->fields() ) {
343             my ($tag) = escape( $field->tag() );
344             if ( $field->is_control_field() ) {
345             my $data = $field->data;
346             push( @xml, qq( ) .
347             escape( ($_transcode ? marc8_to_utf8($data) : $data) ). qq() );
348             } else {
349             my ($i1) = escape( $field->indicator( 1 ) );
350             my ($i2) = escape( $field->indicator( 2 ) );
351             push( @xml, qq( ) );
352             foreach my $subfield ( $field->subfields() ) {
353             my ( $code, $data ) = ( escape( $$subfield[0] ), $$subfield[1] );
354             push( @xml, qq( ).
355             escape( ($_transcode ? marc8_to_utf8($data) : $data) ).qq() );
356             }
357             push( @xml, " " );
358             }
359             }
360             push( @xml, "\n" );
361              
362             if ($_transcode) {
363             substr($ldr,9,1,$original_encoding);
364             $record->leader( $ldr );
365             }
366              
367             return( join( "\n", @xml ) );
368             }
369              
370             my %ESCAPES = (
371             '&' => '&',
372             '<' => '<',
373             '>' => '>',
374             );
375             my $_base_escape_regex = join( '|', map { "\Q$_\E" } keys %ESCAPES );
376             my $ESCAPE_REGEX = qr/$_base_escape_regex/;
377              
378             sub escape {
379             my $string = shift;
380             return '' if ! defined $string or $string eq '';
381             $string =~ s/($ESCAPE_REGEX)/$ESCAPES{$1}/oge;
382             return( $string );
383             }
384              
385             sub _next {
386             my $self = shift;
387             my $fh = $self->{ fh };
388              
389             ## return undef at the end of the file
390             return if eof($fh);
391              
392             ## get a chunk of xml for a record
393             local $/ = 'record>';
394             my $xml = <$fh>;
395              
396             return if $xml =~ m!$!;
397              
398             ## do we have enough?
399             $xml .= <$fh> if $xml !~ m!$!;
400             ## trim stuff before the start record element
401             $xml =~ s/.*?<(([^:]+:){0,1})record.*?>/<$1record>/s;
402              
403             ## return undef if there isn't a good chunk of xml
404             return if ( $xml !~ m|<(([^:]+:){0,1})record>.*|s );
405              
406             ## if we have a namespace prefix, restore the declaration
407             if ($xml =~ /<([^:]+:)record>/) {
408             $xml =~ s!<([^:]+):record>!<$1:record xmlns:$1="http://www.loc.gov/MARC21/slim">!;
409             }
410              
411             ## return the chunk of xml
412             return( $xml );
413             }
414              
415             sub _parser {
416             $parser ||= XML::LibXML->new(
417             ext_ent_handler => sub {
418             die "External entities are not supported\n";
419             }
420             );
421             return $parser;
422             }
423              
424             =head2 decode()
425              
426             You probably don't ever want to call this method directly. If you do
427             you should pass in a chunk of XML as the argument.
428              
429             It is normally invoked by a call to next(), see L or L.
430              
431             =cut
432              
433             sub decode {
434             my $self = shift;
435             my $text;
436             my $location = '';
437              
438             if ( ref($self) =~ /^MARC::File/ ) {
439             $location = 'in record '.$self->{recnum};
440             $text = shift;
441             } else {
442             $location = 'in record 1';
443             $text = $self=~/MARC::File/ ? shift : $self;
444             }
445              
446             my $enc = shift || $_load_args{BinaryEncoding};
447             my $format = shift || $_load_args{RecordFormat};
448              
449             my $parser = _parser();
450             my $xml = $parser->parse_string($text);
451              
452             my $root = $xml->documentElement;
453             croak('MARCXML document has no root element') unless defined $root;
454             if ($root->localname eq 'collection') {
455             my @records = $root->getChildrenByLocalName('record');
456             croak('MARCXML document has no record element') unless @records;
457             $root = $records[0];
458             }
459              
460             my $rec = MARC::Record->new();
461             my @leaders = $root->getElementsByLocalName('leader');
462             my $transcode_to_marc8 = 0;
463             if (@leaders) {
464             my $leader = $leaders[0]->textContent;
465              
466             # this bit is rather questionable
467             $transcode_to_marc8 = substr($leader, 9, 1) eq 'a' && decideMARC8Binary($format, $enc) ? 1 : 0;
468             substr($leader, 9, 1) = ' ' if $transcode_to_marc8;
469            
470             $rec->leader($leader);
471             }
472              
473             my @fields = ();
474             foreach my $elt ($root->getChildrenByLocalName('*')) {
475             if ($elt->localname eq 'controlfield') {
476             push @fields, MARC::Field->new($elt->getAttribute('tag'), $elt->textContent);
477             } elsif ($elt->localname eq 'datafield') {
478             my @sfs = ();
479             foreach my $sfelt ($elt->getChildrenByLocalName('subfield')) {
480             push @sfs, $sfelt->getAttribute('code'),
481             $transcode_to_marc8 ? utf8_to_marc8($sfelt->textContent()) : $sfelt->textContent();
482             }
483             push @fields, MARC::Field->new(
484             $elt->getAttribute('tag'),
485             $elt->getAttribute('ind1'),
486             $elt->getAttribute('ind2'),
487             @sfs
488             );
489             }
490             }
491             $rec->append_fields(@fields);
492             return $rec;
493            
494             }
495              
496             =head2 MARC::File::XML->set_parser($parser)
497              
498             Pass a XML::LibXML parser to MARC::File::XML
499             for it to use. This is optional, meant for
500             use by applications that maintain a shared
501             parser object or which require that external
502             entities be processed. Note that the latter
503             is a potential security risk; see
504             L.
505              
506             =cut
507              
508             sub set_parser {
509             my $self = shift;
510              
511             $parser = shift;
512             undef $parser unless ref($parser) =~ /XML::LibXML/;
513             }
514              
515             sub decideMARC8Binary {
516             my $format = shift;
517             my $enc = shift;
518              
519             return 0 if (defined($format) && lc($format) =~ /^unimarc/o);
520             return 0 if (defined($enc) && lc($enc) =~ /^utf-?8/o);
521             return 1;
522             }
523              
524              
525             =head2 encode()
526              
527             You probably want to use the as_xml() method on your MARC::Record object
528             instead of calling this directly. But if you want to you just need to
529             pass in the MARC::Record object you wish to encode as XML, and you will be
530             returned the XML as a scalar.
531              
532             =cut
533              
534             sub encode {
535             my $record = shift;
536             my $format = shift || $_load_args{RecordFormat};
537             my $without_collection_header = shift;
538             my $enc = shift || $_load_args{DefaultEncoding};
539              
540             if (lc($format) =~ /^unimarc/o) {
541             $enc = _unimarc_encoding( $format => $record );
542             }
543              
544             my @xml = ();
545             push( @xml, header( $enc ) ) unless ($without_collection_header);
546             # verbose, but naming the header output flags this way to avoid
547             # the potential confusion identified in CPAN bug #34082
548             # http://rt.cpan.org/Public/Bug/Display.html?id=34082
549             my $include_full_record_header = ($without_collection_header) ? 1 : 0;
550             push( @xml, record( $record, $format, $include_full_record_header, $enc ) );
551             push( @xml, footer() ) unless ($without_collection_header);
552              
553             return( join( "\n", @xml ) );
554             }
555              
556             sub _unimarc_encoding {
557             my $f = shift;
558             my $r = shift;
559              
560             my $pos = 26;
561             $pos = 13 if (lc($f) eq 'unimarcauth');
562              
563             my $enc = substr( $r->subfield(100 => 'a'), $pos, 2 );
564              
565             if ($enc eq '01' || $enc eq '03') {
566             return 'ISO-8859-1';
567             } elsif ($enc eq '50') {
568             return 'UTF-8';
569             } else {
570             die "Unsupported UNIMARC character encoding [$enc] for XML output for $f; 100\$a -> " . $r->subfield(100 => 'a');
571             }
572             }
573              
574             =head1 TODO
575              
576             =over 4
577              
578             =item * Support for callback filters in decode().
579              
580             =back
581              
582             =head1 SEE ALSO
583              
584             =over 4
585              
586             =item L
587              
588             =item L
589              
590             =item L
591              
592             =item L
593              
594             =back
595              
596             =head1 AUTHORS
597              
598             =over 4
599              
600             =item * Ed Summers
601              
602             =back
603              
604             =cut
605              
606             1;