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