Patch from Galen Charlton, removing $Id$ $Log$ and $Revision$ from files
[koha.git] / C4 / Record.pm
1 package C4::Record;
2 #
3 # Copyright 2006 (C) LibLime
4 # Joshua Ferraro <jmf@liblime.com>
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it under the
9 # terms of the GNU General Public License as published by the Free Software
10 # Foundation; either version 2 of the License, or (at your option) any later
11 # version.
12 #
13 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
19 # Suite 330, Boston, MA  02111-1307 USA
20 #
21 #
22 use strict;# use warnings; #FIXME: turn off warnings before release
23
24 # please specify in which methods a given module is used
25 use MARC::Record; # marc2marcxml, marcxml2marc, html2marc, changeEncoding
26 use MARC::File::XML; # marc2marcxml, marcxml2marc, html2marcxml, changeEncoding
27 use MARC::Crosswalk::DublinCore; # marc2dcxml
28 use Unicode::Normalize; # _entity_encode
29 use XML::LibXSLT;
30 use XML::LibXML;
31
32 use vars qw($VERSION @ISA @EXPORT);
33
34 # set the version for version checking
35 $VERSION = 3.00;
36
37 @ISA = qw(Exporter);
38
39 # only export API methods
40
41 @EXPORT = qw(
42   &marc2marc
43   &marc2marcxml
44   &marcxml2marc
45   &marc2dcxml
46   &marc2modsxml
47
48   &html2marcxml
49   &html2marc
50   &changeEncoding
51 );
52
53 =head1 NAME
54
55 C4::Record - MARC, MARCXML, DC, MODS, XML, etc. Record Management Functions and API
56
57 =head1 SYNOPSIS
58
59 New in Koha 3.x. This module handles all record-related management functions.
60
61 =head1 API (EXPORTED FUNCTIONS)
62
63 =head2 marc2marc - Convert from one flavour of ISO-2709 to another
64
65 =over 4
66
67 my ($error,$newmarc) = marc2marc($marc,$to_flavour,$from_flavour,$encoding);
68
69 Returns an ISO-2709 scalar
70
71 =back
72
73 =cut
74
75 sub marc2marc {
76         my ($marc,$to_flavour,$from_flavour,$encoding) = @_;
77         my $error = "Feature not yet implemented\n";
78         return ($error,$marc);
79 }
80
81 =head2 marc2marcxml - Convert from ISO-2709 to MARCXML
82
83 =over 4
84
85 my ($error,$marcxml) = marc2marcxml($marc,$encoding,$flavour);
86
87 Returns a MARCXML scalar
88
89 =over 2
90
91 C<$marc> - an ISO-2709 scalar or MARC::Record object
92
93 C<$encoding> - UTF-8 or MARC-8 [UTF-8]
94
95 C<$flavour> - MARC21 or UNIMARC
96
97 C<$dont_entity_encode> - a flag that instructs marc2marcxml not to entity encode the xml before returning (optional)
98
99 =back
100
101 =back
102
103 =cut
104
105 sub marc2marcxml {
106         my ($marc,$encoding,$flavour,$dont_entity_encode) = @_;
107         my $error; # the error string
108         my $marcxml; # the final MARCXML scalar
109
110         # test if it's already a MARC::Record object, if not, make it one
111         my $marc_record_obj;
112         if ($marc =~ /^MARC::Record/) { # it's already a MARC::Record object
113                 $marc_record_obj = $marc;
114         } else { # it's not a MARC::Record object, make it one
115                 eval { $marc_record_obj = MARC::Record->new_from_usmarc($marc) }; # handle exceptions
116
117                 # conversion to MARC::Record object failed, populate $error
118                 if ($@) { $error .="\nCreation of MARC::Record object failed: ".$MARC::File::ERROR };
119         }
120         # only proceed if no errors so far
121         unless ($error) {
122
123                 # check the record for warnings
124                 my @warnings = $marc_record_obj->warnings();
125                 if (@warnings) {
126                         warn "\nWarnings encountered while processing ISO-2709 record with title \"".$marc_record_obj->title()."\":\n";
127                         foreach my $warn (@warnings) { warn "\t".$warn };
128                 }
129                 unless($encoding) {$encoding = "UTF-8"}; # set default encoding
130                 unless($flavour) {$flavour = C4::Context->preference("marcflavour")}; # set default MARC flavour
131
132                 # attempt to convert the record to MARCXML
133                 eval { $marcxml = $marc_record_obj->as_xml_record($flavour) }; #handle exceptions
134
135                 # record creation failed, populate $error
136                 if ($@) {
137                         $error .= "Creation of MARCXML failed:".$MARC::File::ERROR;
138                         $error .= "Additional information:\n";
139                         my @warnings = $@->warnings();
140                         foreach my $warn (@warnings) { $error.=$warn."\n" };
141
142                 # record creation was successful
143         } else {
144
145                         # check the record for warning flags again (warnings() will be cleared already if there was an error, see above block
146                         @warnings = $marc_record_obj->warnings();
147                         if (@warnings) {
148                                 warn "\nWarnings encountered while processing ISO-2709 record with title \"".$marc_record_obj->title()."\":\n";
149                                 foreach my $warn (@warnings) { warn "\t".$warn };
150                         }
151                 }
152
153                 # only proceed if no errors so far
154                 unless ($error) {
155
156                         # entity encode the XML unless instructed not to
157                 unless ($dont_entity_encode) {
158                         my ($marcxml_entity_encoded) = _entity_encode($marcxml);
159                         $marcxml = $marcxml_entity_encoded;
160                 }
161                 }
162         }
163         # return result to calling program
164         return ($error,$marcxml);
165 }
166
167 =head2 marcxml2marc - Convert from MARCXML to ISO-2709
168
169 =over 4
170
171 my ($error,$marc) = marcxml2marc($marcxml,$encoding,$flavour);
172
173 Returns an ISO-2709 scalar
174
175 =over 2
176
177 C<$marcxml> - a MARCXML record
178
179 C<$encoding> - UTF-8 or MARC-8 [UTF-8]
180
181 C<$flavour> - MARC21 or UNIMARC
182
183 =back
184
185 =back
186
187 =cut
188
189 sub marcxml2marc {
190     my ($marcxml,$encoding,$flavour) = @_;
191         my $error; # the error string
192         my $marc; # the final ISO-2709 scalar
193         unless($encoding) {$encoding = "UTF-8"}; # set the default encoding
194         unless($flavour) {$flavour = C4::Context->preference("marcflavour")}; # set the default MARC flavour
195
196         # attempt to do the conversion
197         eval { $marc = MARC::Record->new_from_xml($marcxml,$encoding,$flavour) }; # handle exceptions
198
199         # record creation failed, populate $error
200         if ($@) {$error .="\nCreation of MARCXML Record failed: ".$@;
201                 $error.=$MARC::File::ERROR if ($MARC::File::ERROR);
202                 };
203         # return result to calling program
204         return ($error,$marc);
205 }
206
207 =head2 marc2dcxml - Convert from ISO-2709 to Dublin Core
208
209 =over 4
210
211 my ($error,$dcxml) = marc2dcxml($marc,$qualified);
212
213 Returns a DublinCore::Record object, will eventually return a Dublin Core scalar
214
215 FIXME: should return actual XML, not just an object
216
217 =over 2
218
219 C<$marc> - an ISO-2709 scalar or MARC::Record object
220
221 C<$qualified> - specify whether qualified Dublin Core should be used in the input or output [0]
222
223 =back
224
225 =back
226
227 =cut
228
229 sub marc2dcxml {
230         my ($marc,$qualified) = @_;
231         my $error;
232     # test if it's already a MARC::Record object, if not, make it one
233     my $marc_record_obj;
234     if ($marc =~ /^MARC::Record/) { # it's already a MARC::Record object
235         $marc_record_obj = $marc;
236     } else { # it's not a MARC::Record object, make it one
237                 eval { $marc_record_obj = MARC::Record->new_from_usmarc($marc) }; # handle exceptions
238
239                 # conversion to MARC::Record object failed, populate $error
240                 if ($@) {
241                         $error .="\nCreation of MARC::Record object failed: ".$MARC::File::ERROR;
242                 }
243         }
244         my $crosswalk = MARC::Crosswalk::DublinCore->new;
245         if ($qualified) {
246                 $crosswalk = MARC::Crosswalk::DublinCore->new( qualified => 1 );
247         }
248         my $dcxml = $crosswalk->as_dublincore($marc_record_obj);
249         my $dcxmlfinal = "<?xml version=\"1.0\"?>\n";
250         $dcxmlfinal .= "<metadata
251   xmlns=\"http://example.org/myapp/\"
252   xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
253   xsi:schemaLocation=\"http://example.org/myapp/ http://example.org/myapp/schema.xsd\"
254   xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
255   xmlns:dcterms=\"http://purl.org/dc/terms/\">";
256
257         foreach my $element ( $dcxml->elements() ) {
258                 $dcxmlfinal.="<"."dc:".$element->name().">".$element->content()."</"."dc:".$element->name()."\n";
259     }
260         $dcxmlfinal .= "\n</metadata>";
261         return ($error,$dcxmlfinal);
262 }
263 =head2 marc2modsxml - Convert from ISO-2709 to MODS
264
265 =over 4
266
267 my ($error,$modsxml) = marc2modsxml($marc);
268
269 Returns a MODS scalar
270
271 =back
272
273 =cut
274
275 sub marc2modsxml {
276         my ($marc) = @_;
277         # grab the XML, run it through our stylesheet, push it out to the browser
278         my $xmlrecord = marc2marcxml($marc);
279         my $xslfile = C4::Context->config('intranetdir')."/koha-tmpl/intranet-tmpl/prog/en/xslt/MARC21slim2MODS3-1.xsl";
280         my $parser = XML::LibXML->new();
281         my $xslt = XML::LibXSLT->new();
282         my $source = $parser->parse_string($xmlrecord);
283         my $style_doc = $parser->parse_file($xslfile);
284         my $stylesheet = $xslt->parse_stylesheet($style_doc);
285         my $results = $stylesheet->transform($source);
286         my $newxmlrecord = $stylesheet->output_string($results);
287         return ($newxmlrecord);
288 }
289 =head2 html2marcxml
290
291 =over 4
292
293 my ($error,$marcxml) = html2marcxml($tags,$subfields,$values,$indicator,$ind_tag);
294
295 Returns a MARCXML scalar
296
297 this is used in addbiblio.pl and additem.pl to build the MARCXML record from 
298 the form submission.
299
300 FIXME: this could use some better code documentation
301
302 =back
303
304 =cut
305
306 sub html2marcxml {
307     my ($tags,$subfields,$values,$indicator,$ind_tag) = @_;
308         my $error;
309         # add the header info
310     my $marcxml= MARC::File::XML::header(C4::Context->preference('TemplateEncoding'),C4::Context->preference('marcflavour'));
311
312         # some flags used to figure out where in the record we are
313     my $prevvalue;
314     my $prevtag=-1;
315     my $first=1;
316     my $j = -1;
317
318         # handle characters that would cause the parser to choke FIXME: is there a more elegant solution?
319     for (my $i=0;$i<=@$tags;$i++){
320                 @$values[$i] =~ s/&/&amp;/g;
321                 @$values[$i] =~ s/</&lt;/g;
322                 @$values[$i] =~ s/>/&gt;/g;
323                 @$values[$i] =~ s/"/&quot;/g;
324                 @$values[$i] =~ s/'/&apos;/g;
325         
326                 if ((@$tags[$i] ne $prevtag)){
327                         $j++ unless (@$tags[$i] eq "");
328                         #warn "IND:".substr(@$indicator[$j],0,1).substr(@$indicator[$j],1,1)." ".@$tags[$i];
329                         if (!$first){
330                                 $marcxml.="</datafield>\n";
331                                 if ((@$tags[$i] > 10) && (@$values[$i] ne "")){
332                         my $ind1 = substr(@$indicator[$j],0,1);
333                                         my $ind2 = substr(@$indicator[$j],1,1);
334                                         $marcxml.="<datafield tag=\"@$tags[$i]\" ind1=\"$ind1\" ind2=\"$ind2\">\n";
335                                         $marcxml.="<subfield code=\"@$subfields[$i]\">@$values[$i]</subfield>\n";
336                                         $first=0;
337                                 } else {
338                                         $first=1;
339                                 }
340                         } else {
341                                 if (@$values[$i] ne "") {
342                                         # handle the leader
343                                         if (@$tags[$i] eq "000") {
344                                                 $marcxml.="<leader>@$values[$i]</leader>\n";
345                                                 $first=1;
346                                         # rest of the fixed fields
347                                         } elsif (@$tags[$i] < 010) { #FIXME: <10 was the way it was, there might even be a better way
348                                                 $marcxml.="<controlfield tag=\"@$tags[$i]\">@$values[$i]</controlfield>\n";
349                                                 $first=1;
350                                         } else {
351                                                 my $ind1 = substr(@$indicator[$j],0,1);
352                                                 my $ind2 = substr(@$indicator[$j],1,1);
353                                                 $marcxml.="<datafield tag=\"@$tags[$i]\" ind1=\"$ind1\" ind2=\"$ind2\">\n";
354                                                 $marcxml.="<subfield code=\"@$subfields[$i]\">@$values[$i]</subfield>\n";
355                                                 $first=0;
356                                         }
357                                 }
358                         }
359                 } else { # @$tags[$i] eq $prevtag
360                         if (@$values[$i] eq "") {
361                         } else {
362                                 if ($first){
363                                         my $ind1 = substr(@$indicator[$j],0,1);
364                                         my $ind2 = substr(@$indicator[$j],1,1);
365                                         $marcxml.="<datafield tag=\"@$tags[$i]\" ind1=\"$ind1\" ind2=\"$ind2\">\n";
366                                         $first=0;
367                                 }
368                                 $marcxml.="<subfield code=\"@$subfields[$i]\">@$values[$i]</subfield>\n";
369                         }
370                 }
371                 $prevtag = @$tags[$i];
372         }
373         $marcxml.= MARC::File::XML::footer();
374         #warn $marcxml;
375         return ($error,$marcxml);
376 }
377
378 =head2 html2marc
379
380 =over 4
381
382 Probably best to avoid using this ... it has some rather striking problems:
383
384 =over 2
385
386 * saves blank subfields
387
388 * subfield order is hardcoded to always start with 'a' for repeatable tags (because it is hardcoded in the addfield routine).
389
390 * only possible to specify one set of indicators for each set of tags (ie, one for all the 650s). (because they were stored in a hash with the tag as the key).
391
392 * the underlying routines didn't support subfield reordering or subfield repeatability.
393
394 =back 
395
396 I've left it in here because it could be useful if someone took the time to fix it. -- kados
397
398 =back
399
400 =cut
401
402 sub html2marc {
403     my ($dbh,$rtags,$rsubfields,$rvalues,%indicators) = @_;
404     my $prevtag = -1;
405     my $record = MARC::Record->new();
406 #   my %subfieldlist=();
407     my $prevvalue; # if tag <10
408     my $field; # if tag >=10
409     for (my $i=0; $i< @$rtags; $i++) {
410         # rebuild MARC::Record
411 #           warn "0=>".@$rtags[$i].@$rsubfields[$i]." = ".@$rvalues[$i].": ";
412         if (@$rtags[$i] ne $prevtag) {
413             if ($prevtag < 10) {
414                 if ($prevvalue) {
415                     if (($prevtag ne '000') && ($prevvalue ne "")) {
416                         $record->add_fields((sprintf "%03s",$prevtag),$prevvalue);
417                     } elsif ($prevvalue ne ""){
418                         $record->leader($prevvalue);
419                     }
420                 }
421             } else {
422                 if (($field) && ($field ne "")) {
423                     $record->add_fields($field);
424                 }
425             }
426             $indicators{@$rtags[$i]}.='  ';
427                 # skip blank tags, I hope this works
428                 if (@$rtags[$i] eq ''){
429                 $prevtag = @$rtags[$i];
430                 undef $field;
431                 next;
432             }
433             if (@$rtags[$i] <10) {
434                 $prevvalue= @$rvalues[$i];
435                 undef $field;
436             } else {
437                 undef $prevvalue;
438                 if (@$rvalues[$i] eq "") {
439                 undef $field;
440                 } else {
441                 $field = MARC::Field->new( (sprintf "%03s",@$rtags[$i]), substr($indicators{@$rtags[$i]},0,1),substr($indicators{@$rtags[$i]},1,1), @$rsubfields[$i] => @$rvalues[$i]);
442                 }
443 #           warn "1=>".@$rtags[$i].@$rsubfields[$i]." = ".@$rvalues[$i].": ".$field->as_formatted;
444             }
445             $prevtag = @$rtags[$i];
446         } else {
447             if (@$rtags[$i] <10) {
448                 $prevvalue=@$rvalues[$i];
449             } else {
450                 if (length(@$rvalues[$i])>0) {
451                     $field->add_subfields(@$rsubfields[$i] => @$rvalues[$i]);
452 #           warn "2=>".@$rtags[$i].@$rsubfields[$i]." = ".@$rvalues[$i].": ".$field->as_formatted;
453                 }
454             }
455             $prevtag= @$rtags[$i];
456         }
457     }
458     #}
459     # the last has not been included inside the loop... do it now !
460     #use Data::Dumper;
461     #warn Dumper($field->{_subfields});
462     $record->add_fields($field) if (($field) && $field ne "");
463     #warn "HTML2MARC=".$record->as_formatted;
464     return $record;
465 }
466
467 =head2 changeEncoding - Change the encoding of a record
468
469 =over 4
470
471 my ($error, $newrecord) = changeEncoding($record,$format,$flavour,$to_encoding,$from_encoding);
472
473 Changes the encoding of a record
474
475 =over 2
476
477 C<$record> - the record itself can be in ISO-2709, a MARC::Record object, or MARCXML for now (required)
478
479 C<$format> - MARC or MARCXML (required)
480
481 C<$flavour> - MARC21 or UNIMARC, if MARC21, it will change the leader (optional) [defaults to Koha system preference]
482
483 C<$to_encoding> - the encoding you want the record to end up in (optional) [UTF-8]
484
485 C<$from_encoding> - the encoding the record is currently in (optional, it will probably be able to tell unless there's a problem with the record)
486
487 =back 
488
489 FIXME: the from_encoding doesn't work yet
490
491 FIXME: better handling for UNIMARC, it should allow management of 100 field
492
493 FIXME: shouldn't have to convert to and from xml/marc just to change encoding someone needs to re-write MARC::Record's 'encoding' method to actually alter the encoding rather than just changing the leader
494
495 =back
496
497 =cut
498
499 sub changeEncoding {
500         my ($record,$format,$flavour,$to_encoding,$from_encoding) = @_;
501         my $newrecord;
502         my $error;
503         unless($flavour) {$flavour = C4::Context->preference("marcflavour")};
504         unless($to_encoding) {$to_encoding = "UTF-8"};
505         
506         # ISO-2709 Record (MARC21 or UNIMARC)
507         if (lc($format) =~ /^marc$/o) {
508                 # if we're converting encoding of an ISO2709 file, we need to roundtrip through XML
509                 #       because MARC::Record doesn't directly provide us with an encoding method
510                 #       It's definitely less than idea and should be fixed eventually - kados
511                 my $marcxml; # temporary storage of MARCXML scalar
512                 ($error,$marcxml) = marc2marcxml($record,$to_encoding,$flavour);
513                 unless ($error) {
514                         ($error,$newrecord) = marcxml2marc($marcxml,$to_encoding,$flavour);
515                 }
516         
517         # MARCXML Record
518         } elsif (lc($format) =~ /^marcxml$/o) { # MARCXML Record
519                 my $marc;
520                 ($error,$marc) = marcxml2marc($record,$to_encoding,$flavour);
521                 unless ($error) {
522                         ($error,$newrecord) = marc2marcxml($record,$to_encoding,$flavour);
523                 }
524         } else {
525                 $error.="Unsupported record format:".$format;
526         }
527         return ($error,$newrecord);
528 }
529
530 =head1 INTERNAL FUNCTIONS
531
532 =head2 _entity_encode - Entity-encode an array of strings
533
534 =over 4
535
536 my ($entity_encoded_string) = _entity_encode($string);
537
538 or
539
540 my (@entity_encoded_strings) = _entity_encode(@strings);
541
542 Entity-encode an array of strings
543
544 =back
545
546 =cut
547
548 sub _entity_encode {
549         my @strings = @_;
550         my @strings_entity_encoded;
551         foreach my $string (@strings) {
552                 my $nfc_string = NFC($string);
553                 $nfc_string =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
554                 push @strings_entity_encoded, $nfc_string;
555         }
556         return @strings_entity_encoded;
557 }
558
559 END { }       # module clean-up code here (global destructor)
560 1;
561 __END__
562
563 =head1 AUTHOR
564
565 Joshua Ferraro <jmf@liblime.com>
566
567 =head1 MODIFICATIONS
568
569
570 =cut