]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Cat/BibCommon.pm
3dd3d32118494bca985d654613938054b367bde8
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Cat / BibCommon.pm
1 package OpenILS::Application::Cat::BibCommon;
2 use strict; use warnings;
3 use OpenILS::Utils::CStoreEditor q/:funcs/;
4 use OpenSRF::Utils::Logger qw($logger);
5 use OpenILS::Application::AppUtils;
6 use OpenILS::Utils::Fieldmapper;
7 use OpenILS::Const qw/:const/;
8 use OpenSRF::AppSession;
9 use OpenILS::Event;
10 my $U = 'OpenILS::Application::AppUtils';
11 my $MARC_NAMESPACE = 'http://www.loc.gov/MARC21/slim';
12
13
14 # ---------------------------------------------------------------------------
15 # Shared bib mangling code.  Do not publish methods from here.
16 # ---------------------------------------------------------------------------
17
18 my $__bib_sources;
19 sub bib_source_from_name {
20         my $name = shift;
21         $logger->debug("searching for bib source: $name");
22
23         fetch_bib_sources();
24
25         my ($s) = grep { lc($_->source) eq lc($name) } @$__bib_sources;
26
27         return $s->id if $s;
28         return undef;
29 }
30
31 sub fetch_bib_sources {
32         $__bib_sources = new_editor()->retrieve_all_config_bib_source()
33                 unless $__bib_sources;
34         return $__bib_sources;
35 }
36
37
38 sub biblio_record_replace_marc  {
39         my($class, $e, $recid, $newxml, $source, $fixtcn, $override) = @_;
40
41         my $rec = $e->retrieve_biblio_record_entry($recid)
42                 or return $e->die_event;
43
44     # See if there is a different record in the database that has our TCN value
45     # If we're not updating the TCN, all we care about it the marcdoc
46     # XXX should .update even bother with the tcn_info if it's not going to replace it?
47     # there is the potential for returning a TCN_EXISTS event, even though no replacement happens
48
49         my( $tcn, $tsource, $marcdoc, $evt);
50
51     if($fixtcn or $override) {
52
53             ($tcn, $tsource, $marcdoc, $evt) = 
54                     _find_tcn_info($e, $newxml, $override, $recid);
55
56             return $evt if $evt;
57
58                 $rec->tcn_value($tcn) if ($tcn);
59                 $rec->tcn_source($tsource);
60
61     } else {
62
63         $marcdoc = __make_marc_doc($newxml);
64     }
65
66
67         $rec->source(bib_source_from_name($source)) if $source;
68         $rec->editor($e->requestor->id);
69         $rec->edit_date('now');
70         $rec->marc( $U->entityize( $marcdoc->documentElement->toString ) );
71         $e->update_biblio_record_entry($rec) or return $e->die_event;
72
73         return $rec;
74 }
75
76 sub biblio_record_xml_import {
77         my($class, $e, $xml, $source, $auto_tcn, $override) = @_;
78
79         my( $evt, $tcn, $tcn_source, $marcdoc );
80
81         if( $auto_tcn ) {
82                 # auto_tcn forces a blank TCN value so the DB will have to generate one for us
83                 $marcdoc = __make_marc_doc($xml);
84         } else {
85                 ( $tcn, $tcn_source, $marcdoc, $evt ) = _find_tcn_info($e, $xml, $override);
86                 return $evt if $evt;
87         }
88
89         $logger->info("user ".$e->requestor->id.
90                 " creating new biblio entry with tcn=$tcn and tcn_source $tcn_source");
91
92         my $record = Fieldmapper::biblio::record_entry->new;
93
94         $record->source(bib_source_from_name($source)) if $source;
95         $record->tcn_source($tcn_source);
96         $record->tcn_value($tcn) if ($tcn);
97         $record->creator($e->requestor->id);
98         $record->editor($e->requestor->id);
99         $record->create_date('now');
100         $record->edit_date('now');
101         $record->marc($U->entityize($marcdoc->documentElement->toString));
102
103     $record = $e->create_biblio_record_entry($record) or return $e->die_event;
104         $logger->info("marc create/import created new record ".$record->id);
105
106         return $record;
107 }
108
109 sub __make_marc_doc {
110         my $xml = shift;
111         my $marcxml = XML::LibXML->new->parse_string($xml);
112         $marcxml->documentElement->setNamespace($MARC_NAMESPACE, "marc", 1 );
113         $marcxml->documentElement->setNamespace($MARC_NAMESPACE);
114         __remove_empty_marc_nodes($marcxml);
115         return $marcxml;
116 }
117
118 # remove empty control fields, subfields, and variable data fields, which
119 # can creep in via less-than-correct imported MARC records or issues
120 # with templates
121 sub __remove_empty_marc_nodes {
122         my $marcxml = shift;
123
124         __remove_if_childless($_) foreach $marcxml->documentElement->getElementsByTagNameNS($MARC_NAMESPACE, 'controlfield');
125         __remove_if_childless($_) foreach $marcxml->documentElement->getElementsByTagNameNS($MARC_NAMESPACE, 'subfield');
126         __remove_if_childless($_) foreach $marcxml->documentElement->getElementsByTagNameNS($MARC_NAMESPACE, 'datafield');
127 }
128
129 sub __remove_if_childless {
130         my $node = shift;
131         my @children = $node->childNodes();
132         my $has_nonblank_children = 0;
133         # can do this more concisely by requiring XML::LibXML >= 1.70 and using nonBlankChildNodes()
134         foreach my $node ($node->childNodes()) {
135                 if ($node->nodeType != XML::LibXML::XML_TEXT_NODE || $node->nodeValue !~ /^\s*$/) {
136                         $has_nonblank_children = 1;
137                         last;
138                 }
139         }
140         $node->parentNode->removeChild($node) unless $has_nonblank_children;
141 }
142
143 sub _find_tcn_info { 
144         my $editor              = shift;
145         my $xml                 = shift;
146         my $override    = shift;
147         my $existing_rec        = shift || 0;
148
149         # parse the XML
150         my $marcxml = __make_marc_doc($xml);
151
152         my $xpath = '//marc:controlfield[@tag="001"]';
153         my $tcn = $marcxml->documentElement->findvalue($xpath);
154         $logger->info("biblio import located 001 (tcn) value of $tcn");
155
156         $xpath = '//marc:controlfield[@tag="003"]';
157         my $tcn_source = $marcxml->documentElement->findvalue($xpath) || "System Local";
158
159         if(my $rec = _tcn_exists($editor, $tcn, $tcn_source, $existing_rec) ) {
160
161                 my $origtcn = $tcn;
162                 $tcn = find_free_tcn( $marcxml, $editor, $existing_rec );
163
164                 # if we're overriding, try to find a different TCN to use
165                 if( $override ) {
166
167          # XXX Create ALLOW_ALT_TCN permission check support 
168
169                         $logger->info("tcn value $tcn already exists, attempting to override");
170
171                         if(!$tcn) {
172                                 return ( 
173                                         undef, 
174                                         undef, 
175                                         undef,
176                                         OpenILS::Event->new(
177                                                 'OPEN_TCN_NOT_FOUND', 
178                                                         payload => $marcxml->toString())
179                                         );
180                         }
181
182                 } else {
183
184                         $logger->warn("tcn value $origtcn already exists in import/create");
185
186                         # otherwise, return event
187                         return ( 
188                                 undef, 
189                                 undef, 
190                                 undef,
191                                 OpenILS::Event->new( 
192                                         'TCN_EXISTS', payload => { 
193                                                 dup_record      => $rec, 
194                                                 tcn                     => $origtcn,
195                                                 new_tcn         => $tcn
196                                                 }
197                                         )
198                                 );
199                 }
200         }
201
202         return ($tcn, $tcn_source, $marcxml);
203 }
204
205 sub find_free_tcn {
206
207         my $marcxml = shift;
208         my $editor = shift;
209         my $existing_rec = shift;
210
211         my $add_901 = 0;
212
213         my $xpath = '//marc:datafield[@tag="901"]/marc:subfield[@code="a"]';
214         my ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
215
216     if (!$tcn) {
217             $xpath = '//marc:datafield[@tag="039"]/marc:subfield[@code="a"]';
218             ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
219     }
220
221         $xpath = '//marc:datafield[@tag="901"]/marc:subfield[@code="b"]';
222         my ($tcn_source) = $marcxml->documentElement->findvalue($xpath);
223     if (!$tcn_source) {
224             $xpath = '//marc:datafield[@tag="039"]/marc:subfield[@code="b"]';
225         $tcn_source = $marcxml->documentElement->findvalue($xpath) || "System Local";
226     }
227
228         if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {
229                 $tcn = undef;
230         } else {
231                 $add_901++;
232         }
233
234
235         if(!$tcn) {
236                 $xpath = '//marc:datafield[@tag="020"]/marc:subfield[@code="a"]';
237                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
238                 $tcn_source = "ISBN";
239                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
240         }
241
242         if(!$tcn) { 
243                 $xpath = '//marc:datafield[@tag="022"]/marc:subfield[@code="a"]';
244                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
245                 $tcn_source = "ISSN";
246                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
247         }
248
249         if(!$tcn) {
250                 $xpath = '//marc:datafield[@tag="010"]';
251                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
252                 $tcn_source = "LCCN";
253                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
254         }
255
256         if(!$tcn) {
257                 $xpath = '//marc:datafield[@tag="035"]/marc:subfield[@code="a"]';
258                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
259                 $tcn_source = "System Legacy";
260                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
261
262                 if($tcn) {
263                         $marcxml->documentElement->removeChild(
264                                 $marcxml->documentElement->findnodes( '//marc:datafield[@tag="035"]' )
265                         );
266                 }
267         }
268
269         return undef unless $tcn;
270
271         if ($add_901) {
272                 my $df = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'datafield');
273                 $df->setAttribute( tag => '901' );
274                 $df->setAttribute( ind1 => ' ' );
275                 $df->setAttribute( ind2 => ' ' );
276                 $marcxml->documentElement->appendChild( $df );
277
278                 my $sfa = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'subfield');
279                 $sfa->setAttribute( code => 'a' );
280                 $sfa->appendChild( $marcxml->createTextNode( $tcn ) );
281                 $df->appendChild( $sfa );
282
283                 my $sfb = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'subfield');
284                 $sfb->setAttribute( code => 'b' );
285                 $sfb->appendChild( $marcxml->createTextNode( $tcn_source ) );
286                 $df->appendChild( $sfb );
287
288         if ($existing_rec) {
289                 my $sfc = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'subfield');
290                 $sfc->setAttribute( code => 'c' );
291                 $sfc->appendChild( $marcxml->createTextNode( $existing_rec ) );
292                     $df->appendChild( $sfb );
293         }
294         }
295
296         return $tcn;
297 }
298
299
300
301 sub _tcn_exists {
302         my $editor = shift;
303         my $tcn = shift;
304         my $source = shift;
305         my $existing_rec = shift || 0;
306
307         if(!$tcn) {return 0;}
308
309         $logger->debug("tcn_exists search for tcn $tcn and source $source and id $existing_rec");
310
311         # XXX why does the source matter?
312 #       my $req = $session->request(      
313 #               { tcn_value => $tcn, tcn_source => $source, deleted => 'f' } );
314
315     my $recs = $editor->search_biblio_record_entry(
316         {tcn_value => $tcn, deleted => 'f', id => {'!=' => $existing_rec}}, {idlist =>1});
317
318         if(@$recs) {
319                 $logger->debug("_tcn_exists is true for tcn : $tcn ($source)");
320                 return $recs->[0];
321         }
322
323         $logger->debug("_tcn_exists is false for tcn : $tcn ($source)");
324         return 0;
325 }
326
327
328 sub delete_rec {
329    my($class, $editor, $rec_id ) = @_;
330
331    my $rec = $editor->retrieve_biblio_record_entry($rec_id)
332       or return $editor->event;
333
334    return undef if $U->is_true($rec->deleted);
335    
336    $rec->deleted('t');
337    $rec->active('f');
338    $rec->editor( $editor->requestor->id );
339    $rec->edit_date('now');
340
341    # Set the leader/05 to indicate that the record has been deleted
342    my $marc = $rec->marc();
343    $marc =~ s{(<leader>.{5}).}{$1d};
344    $rec->marc($marc);
345
346    $editor->update_biblio_record_entry($rec) or return $editor->event;
347
348    return undef;
349 }
350
351
352 # ---------------------------------------------------------------------------
353 # returns true if the given title (id) has no un-deleted volumes or 
354 # copies attached.  If a context volume is defined, a record
355 # is considered empty only if the context volume is the only
356 # remaining volume on the record.  
357 # ---------------------------------------------------------------------------
358 sub title_is_empty {
359         my($class, $editor, $rid, $vol_id) = @_;
360
361         return 0 if $rid == OILS_PRECAT_RECORD;
362
363         my $cnlist = $editor->search_asset_call_number(
364                 { record => $rid, deleted => 'f' }, { idlist => 1 } );
365
366         return 1 unless @$cnlist; # no attached volumes
367     return 0 if @$cnlist > 1; # multiple attached volumes
368     return 0 unless $$cnlist[0] == $vol_id; # attached volume is not the context vol.
369
370     # see if the sole remaining context volume has any attached copies
371         for my $cn (@$cnlist) {
372                 my $copylist = $editor->search_asset_copy(
373                         [
374                                 { call_number => $cn, deleted => 'f' }, 
375                                 { limit => 1 },
376                         ], { idlist => 1 });
377                 return 0 if @$copylist; # false if we find any copies
378         }
379
380         return 1;
381 }
382 1;