]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Utils/ModsParser.pm
Post-2.5-m1 whitespace fixup
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Utils / ModsParser.pm
1 package OpenILS::Utils::ModsParser;
2 use strict; use warnings;
3
4 use OpenSRF::EX qw/:try/;
5 use XML::LibXML;
6 use XML::LibXSLT;
7 use Time::HiRes qw(time);
8 use OpenILS::Utils::Fieldmapper;
9 use OpenSRF::Utils::SettingsClient;
10 use OpenSRF::Utils::Logger qw/$logger/;
11 use Data::Dumper;
12
13 my $parser      = XML::LibXML->new();
14 my $xslt            = XML::LibXSLT->new();
15 my $mods_sheet;
16
17 # ----------------------------------------------------------------------------------------
18 # XPATH for extracting info from a MODS doc
19 my $isbn_xpath          = "//mods:mods/mods:identifier[\@type='isbn' and not(\@invalid)]";
20 my $resource_xpath  = "//mods:mods/mods:typeOfResource";
21 my $pub_xpath           = "//mods:mods/mods:originInfo//mods:dateIssued[\@encoding='marc']|" . 
22                                 "//mods:mods/mods:originInfo//mods:dateIssued[1]";
23 my $tcn_xpath           = "//mods:mods/mods:recordInfo/mods:recordIdentifier";
24 my $publisher_xpath = "//mods:mods/mods:originInfo//mods:publisher[1]";
25 my $edition_xpath       = "//mods:mods/mods:originInfo//mods:edition[1]";
26 my $abstract_xpath  = "//mods:mods/mods:abstract";
27 my $related_xpath       = "";
28 my $online_loc_xpath = "//mods:location/mods:url";
29 my $physical_desc       = "(//mods:mods/mods:physicalDescription/mods:form|//mods:mods/mods:physicalDescription/mods:extent|".
30     "//mods:mods/mods:physicalDescription/mods:reformattingQuality|//mods:mods/mods:physicalDescription/mods:internetMediaType|".
31     "//mods:mods/mods:physicalDescription/mods:digitalOrigin)";
32 my $toc_xpath           = "//mods:tableOfContents";
33
34 my $xpathset = {
35
36     title => {
37         abbreviated => 
38             "//mods:mods/mods:titleInfo[mods:title and (\@type='abbreviated')]",
39         translated =>
40             "//mods:mods/mods:titleInfo[mods:title and (\@type='translated')]",
41         uniform =>
42             "//mods:mods/mods:titleInfo[mods:title and (\@type='uniform')]",
43         proper =>
44             "//mods:mods/mods:titleInfo[mods:title and not (\@type)]",
45         any =>
46             "//mods:mods/mods:titleInfo",
47     },
48
49     author => {
50         corporate => 
51             "//mods:mods/mods:name[\@type='corporate']/*[local-name()='namePart']".
52                 "[../mods:role/mods:text[text()='creator']".
53                 " or ../mods:role/mods:roleTerm[".
54                 "        \@type='text'".
55                 "        and \@authority='marcrelator'".
56                 "        and text()='creator']".
57                 "][1]",
58         personal => 
59             "//mods:mods/mods:name[\@type='personal']/*[local-name()='namePart']".
60                 "[../mods:role/mods:text[text()='creator']".
61                 " or ../mods:role/mods:roleTerm[".
62                 "        \@type='text'".
63                 "        and \@authority='marcrelator'".
64                 "        and text()='creator']".
65                 "][1]",
66         conference => 
67             "//mods:mods/mods:name[\@type='conference']/*[local-name()='namePart']".
68                 "[../mods:role/mods:text[text()='creator']".
69                 " or ../mods:role/mods:roleTerm[".
70                 "        \@type='text'".
71                 "        and \@authority='marcrelator'".
72                 "        and text()='creator']".
73                 "][1]",
74         other => 
75             "//mods:mods/mods:name[\@type='personal']/*[local-name()='namePart']",
76         any => 
77             "//mods:mods/mods:name/*[local-name()='namePart'][1]",
78     },
79
80     subject => {
81
82         topic => 
83             "//mods:mods/mods:subject/*[".
84             "   local-name()='geographic'".
85             "   or local-name()='name'".
86             "   or local-name()='temporal'".
87             "   or local-name()='topic'".
88             "]/parent::mods:subject",
89
90 #       geographic => 
91 #           "//mods:mods/*[local-name()='subject']/*[local-name()='geographic']",
92 #       name => 
93 #           "//mods:mods/*[local-name()='subject']/*[local-name()='name']",
94 #       temporal => 
95 #           "//mods:mods/*[local-name()='subject']/*[local-name()='temporal']",
96 #       topic => 
97 #           "//mods:mods/*[local-name()='subject']/*[local-name()='topic']",
98     },
99     #keyword => { keyword => "//mods:mods/*[not(local-name()='originInfo')]", },
100
101     series => {
102         series => "//mods:mods/mods:relatedItem[\@type='series']/mods:titleInfo"
103     }
104 };
105 # ----------------------------------------------------------------------------------------
106
107
108
109 sub new { return bless( {}, shift() ); }
110
111 sub get_field_value {
112
113     my( $self, $mods, $xpath, $type) = @_;
114
115     my @string;
116
117     my $root = $mods->documentElement;
118     $root->setNamespace( "http://www.loc.gov/mods/v3", "mods", 1 );
119
120     try {
121         # grab the set of matching nodes
122         my @nodes = $root->findnodes( $xpath );
123         for my $value (@nodes) {
124
125             # grab all children of the node
126             my @children = $value->childNodes();
127             my @child_text;
128             for my $child (@children) {
129                 # MODS strips the punctuation from 245abc, which often
130                 # results in "title subtitle" rather than "title : subtitle";
131                 # this hack gets it back for us
132                 if ($type && $type eq 'title' && $child->nodeName =~ m/subTitle/) {
133                     push(@child_text, " : "); 
134                 }
135                 next unless( $child->nodeType != 3 );
136
137                 if($child->childNodes) {
138                     my @a;
139                     for my $c (@{$child->childNodes}){
140                         push @a, $c->textContent;
141                     }
142                     push(@child_text, join(' ', @a));
143
144                 } else {
145                     push(@child_text, $child->textContent); 
146                 }
147
148             }
149             if(@child_text) {
150                 push(@string, \@child_text);
151             }
152
153             if( !@child_text  ) {
154                 push(@string, $value->textContent );
155             }
156         }
157     } otherwise {
158         $logger->info("MODS-izing failure: ".shift());
159         $logger->info("Failed MODS xml: ".$root->toString);
160         $logger->info("Failed MODS xpath: $xpath");
161     };
162     return @string;
163 }
164
165 =head1 old implementation
166
167 sub _modsdoc_to_values {
168     my( $self, $mods ) = @_;
169     my $data = {};
170     for my $class (keys %$xpathset) {
171         $data->{$class} = {};
172         for my $type(keys %{$xpathset->{$class}}) {
173             my @value = $self->get_field_value( $mods, $xpathset->{$class}->{$type} );
174             if( $class eq "subject" ) {
175                 push( @{$data->{$class}->{$type}},  @value );
176             } else {
177                 $data->{$class}->{$type} = $value[0];
178             }
179         }
180     }
181     return $data;
182 }
183
184 =cut
185
186 sub modsdoc_to_values {
187     my( $self, $mods ) = @_;
188     my $data = {};
189
190     {
191         my $class = "subject";
192         $data->{$class} = {};
193         for my $type(keys %{$xpathset->{$class}}) {
194             my @value = $self->get_field_value( $mods, $xpathset->{$class}->{$type} );
195             for my $arr (@value) {
196                 push( @{$data->{$class}->{$type}},  $arr);
197             }
198         }
199     }
200
201     {
202         my $class = "title";
203         $data->{$class} = {};
204         for my $type(keys %{$xpathset->{$class}}) {
205             my @value = $self->get_field_value( $mods, $xpathset->{$class}->{$type}, "title" );
206             for my $arr (@value) {
207                 if( ref($arr) ) {
208                     $data->{$class}->{$type} = shift @$arr;
209
210                     my $t = lc($data->{$class}->{$type});
211                     if($t and $t =~ /^l[eoa]s|l[ae]|el|the|un[ae]?|an?\s?$/o ) {
212                         my $val = shift @$arr || "";
213                         $data->{$class}->{$type} .= " $val" if $data->{$class}->{$type};
214                         $data->{$class}->{$type} = " $val" unless $data->{$class}->{$type};
215                     }
216
217                     for my $t (@$arr) {
218                         $data->{$class}->{$type} .= " $t";
219                     }
220                 } else {
221                     $data->{$class}->{$type} = $arr;
222                 }
223             }
224             $data->{$class}->{$type} =~ s/\s+/ /go if ($data->{$class}->{$type});
225         }
226     }
227
228     {
229         my $class = "author";
230         $data->{$class} = {};
231         for my $type(keys %{$xpathset->{$class}}) {
232             my @value = $self->get_field_value( $mods, $xpathset->{$class}->{$type} );
233             $data->{$class}->{$type} = $value[0];
234         }
235     }
236
237     {
238         my $class = "series";
239         $data->{$class} = {};
240         for my $type(keys %{$xpathset->{$class}}) {
241             my @value = $self->get_field_value( $mods, $xpathset->{$class}->{$type} );
242             for my $arr (@value) {
243                 if( ref($arr) ) {
244                     push(@{$data->{$class}->{$type}}, join(" ", @$arr));
245                 } else {
246                     push( @{$data->{$class}->{$type}}, $arr );
247                 }
248             }
249         }
250
251     }
252
253     return $data;
254 }
255
256
257
258
259 # ---------------------------------------------------------------------------
260 # Grabs the data 'we want' from the MODS doc and returns it in hash form
261 # ---------------------------------------------------------------------------
262 sub mods_values_to_mods_slim {
263     my( $self, $modsperl ) = @_;
264
265     my $title = "";
266     my $author = "";
267     my $subject = [];
268     my $series  = [];
269
270     my $tmp = $modsperl->{title};
271
272
273     if(!$tmp) { $title = ""; }
274     else {
275         ($title = $tmp->{proper}) ||
276         ($title = $tmp->{translated}) ||
277         ($title = $tmp->{abbreviated}) ||
278         ($title = $tmp->{uniform}) ||
279         ($title = $tmp->{any});
280     }
281
282     $tmp = $modsperl->{author};
283     if(!$tmp) { $author = ""; }
284     else {
285         ($author = $tmp->{personal}) ||
286         ($author = $tmp->{corporate}) ||
287         ($author = $tmp->{conference}) ||
288         ($author = $tmp->{other}) ||
289         ($author = $tmp->{any}); 
290     }
291
292     $tmp = $modsperl->{subject};
293     if(!$tmp) { $subject = {}; } 
294     else {
295         for my $key( keys %{$tmp}) {
296             push(@$subject, @{$tmp->{$key}}) if ($tmp->{$key});
297         }
298         my $subh = {};
299         for my $s (@$subject) {
300             if(defined($subh->{$s})) { $subh->{$s->[0]}++ } else { $subh->{$s->[0]} = 1;}
301         }
302         $subject = $subh
303     }
304
305     $tmp = $modsperl->{'series'};
306     if(!$tmp) { $series = []; }
307     else { $series = $tmp->{'series'}; }
308
309
310     return { series => $series, title => $title, 
311             author => $author, subject => $subject };
312 }
313
314
315
316 # ---------------------------------------------------------------------------
317 # Initializes a MARC -> Unified MODS batch process
318 # ---------------------------------------------------------------------------
319
320 sub start_mods_batch {
321
322     my( $self, $master_doc ) = @_;
323
324     if(!$master_doc) {
325         $self->{master_doc} = undef;
326         return;
327     }
328
329     if(!$mods_sheet) {
330          my $xslt_doc = $parser->parse_file(
331             OpenSRF::Utils::SettingsClient->new->config_value(dirs => 'xsl') .  "/MARC21slim2MODS32.xsl");
332         $mods_sheet = $xslt->parse_stylesheet( $xslt_doc );
333     }
334
335
336     my $xmldoc = $parser->parse_string($master_doc);
337     my $mods = $mods_sheet->transform($xmldoc);
338
339     $self->{master_doc} = $self->modsdoc_to_values( $mods );
340     $self->{master_doc} = $self->mods_values_to_mods_slim( $self->{master_doc} );
341
342     ($self->{master_doc}->{isbn}) = 
343         $self->get_field_value( $mods, $isbn_xpath );
344
345     $self->{master_doc}->{type_of_resource} = 
346         [ $self->get_field_value( $mods, $resource_xpath ) ];
347
348     ($self->{master_doc}->{tcn}) = 
349         $self->get_field_value( $mods, $tcn_xpath );
350
351     ($self->{master_doc}->{pubdate}) = 
352         $self->get_field_value( $mods, $pub_xpath );
353
354     ($self->{master_doc}->{publisher}) = 
355         $self->get_field_value( $mods, $publisher_xpath );
356
357     ($self->{master_doc}->{edition}) =
358         $self->get_field_value( $mods, $edition_xpath );
359
360
361
362 # ------------------------------
363     # holds an array of [ link, title, link, title, ... ]
364     $self->{master_doc}->{online_loc} = [];
365     for my $url ($mods->findnodes($online_loc_xpath)) {
366         push(@{$self->{master_doc}->{online_loc}}, $url->textContent);
367         push(@{$self->{master_doc}->{online_loc}}, $url->getAttribute('displayLabel') || '');
368         push(@{$self->{master_doc}->{online_loc}}, $url->getAttribute('note') || '');
369     }
370
371     ($self->{master_doc}->{synopsis}) = 
372         $self->get_field_value( $mods, $abstract_xpath );
373
374     $self->{master_doc}->{physical_description} = [];
375     push(@{$self->{master_doc}->{physical_description}},
376         $self->get_field_value( $mods, $physical_desc ) );
377     $self->{master_doc}->{physical_description} = 
378         join( ' ', @{$self->{master_doc}->{physical_description}});
379
380     ($self->{master_doc}->{toc}) = $self->get_field_value($mods, $toc_xpath);
381
382 }
383
384
385
386 # ---------------------------------------------------------------------------
387 # Takes a MARCXML string and adds it to the growing MODS doc
388 # ---------------------------------------------------------------------------
389 sub push_mods_batch {
390     my( $self, $marcxml ) = @_;
391
392     my $xmldoc = $parser->parse_string($marcxml);
393     my $mods = $mods_sheet->transform($xmldoc);
394
395     my $xmlperl = $self->modsdoc_to_values( $mods );
396     $xmlperl = $self->mods_values_to_mods_slim( $xmlperl );
397
398     # for backwards compatibility, remove the array part when all is decided
399     if(ref($xmlperl->{subject}) eq 'ARRAY' ) {
400         for my $subject( @{$xmlperl->{subject}} ) {
401             push @{$self->{master_doc}->{subject}}, $subject;
402         }
403     } else {
404         for my $subject ( keys %{$xmlperl->{subject}} ) {
405             my $s = $self->{master_doc}->{subject};
406             if(defined($s->{$subject})) { $s->{$subject}++; } else { $s->{$subject} = 1; }
407         }
408     }
409
410     push( @{$self->{master_doc}->{type_of_resource}}, 
411         $self->get_field_value( $mods, $resource_xpath ));
412
413     if(!($self->{master_doc}->{isbn}) ) {
414         ($self->{master_doc}->{isbn}) = 
415             $self->get_field_value( $mods, $isbn_xpath );
416     }
417 }
418
419
420 # ---------------------------------------------------------------------------
421 # Completes a MARC -> Unified MODS batch process and returns the perl hash
422 # ---------------------------------------------------------------------------
423 sub init_virtual_record {
424     my $record = Fieldmapper::metabib::virtual_record->new;
425     $record->subject([]);
426     $record->types_of_resource([]);
427     $record->call_numbers([]);
428     return $record;
429 }
430
431 sub finish_mods_batch {
432     my $self = shift;
433
434     return undef unless $self->{master_doc};
435
436     my $perl = $self->{master_doc};
437     my $record = init_virtual_record();
438
439     # turn the hash into a fieldmapper object
440     #(my $title = $perl->{title}) =~ s/\[.*?\]//og;
441     #(my $author = $perl->{author}) =~ s/\(.*?\)//og;
442     my $title = $perl->{title};
443     my $author = $perl->{author};
444
445     my @series;
446     for my $s (@{$perl->{series}}) {
447         push @series, (split( /\s*;/, $s ))[0];
448     }
449
450     # uniquify the types of resource
451     my $rtypes = $perl->{type_of_resource};
452     my %hash = map { ($_ => 1) } @$rtypes;
453     $rtypes = [ keys %hash ];
454
455     $record->title($title);
456     $record->author($author);
457
458     $record->doc_id($perl->{doc_id});
459     $record->isbn($perl->{isbn});
460     $record->pubdate($perl->{pubdate});
461     $record->publisher($perl->{publisher});
462     $record->tcn($perl->{tcn});
463
464     $record->edition($perl->{edition});
465
466     $record->subject($perl->{subject});
467     $record->types_of_resource($rtypes);
468     $record->series(\@series);
469
470     $record->online_loc($perl->{online_loc});
471     $record->synopsis($perl->{synopsis});
472     $record->physical_description($perl->{physical_description});
473     $record->toc($perl->{toc});
474
475     $self->{master_doc} = undef;
476     return $record;
477 }
478
479
480