6a776d8158fa332ebb671fe1e509e22e2e2d6e7b
[Evergreen.git] / Open-ILS / src / perlmods / lib / 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     my $use_id = $e->retrieve_config_global_flag('cat.bib.use_id_for_tcn');
82     $use_id = ($use_id and $U->is_true($use_id->enabled));
83
84         if( $auto_tcn or $use_id ) {
85                 # auto_tcn forces a blank TCN value so the DB will have to generate one for us
86                 $marcdoc = __make_marc_doc($xml);
87         } else {
88                 ( $tcn, $tcn_source, $marcdoc, $evt ) = _find_tcn_info($e, $xml, $override);
89                 return $evt if $evt;
90         }
91
92         # Silence warnings when _find_tcn_info() fails
93         $tcn ||= '';
94         $tcn_source ||= '';
95         $logger->info("user ".$e->requestor->id.
96                 " creating new biblio entry with tcn=$tcn and tcn_source $tcn_source");
97
98         my $record = Fieldmapper::biblio::record_entry->new;
99
100         $record->source(bib_source_from_name($source)) if $source;
101         $record->tcn_source($tcn_source);
102         $record->tcn_value($tcn) if ($tcn);
103         $record->creator($e->requestor->id);
104         $record->editor($e->requestor->id);
105         $record->create_date('now');
106         $record->edit_date('now');
107         $record->marc($U->entityize($marcdoc->documentElement->toString));
108
109     $record = $e->create_biblio_record_entry($record) or return $e->die_event;
110
111     if($use_id) {
112         my $existing = $e->search_biblio_record_entry(
113             {   
114                 tcn_value => $record->id,
115                 deleted => 'f'
116             }, { 
117                 idlist => 1 
118             }
119         );
120
121         if(@$existing) {
122             # leave the auto-generated tcn_value in place
123             $logger->warn("Collision using internal ID as tcn_value for record " . $record->id);
124         } else {
125             $record->tcn_value($record->id);
126             $e->update_biblio_record_entry($record) or return $e->die_event;
127         }
128     }
129
130         $logger->info("marc create/import created new record ".$record->id);
131         return $record;
132 }
133
134 sub __make_marc_doc {
135         my $xml = shift;
136         my $marcxml = XML::LibXML->new->parse_string($xml);
137         $marcxml->documentElement->setNamespace($MARC_NAMESPACE, "marc", 1 );
138         $marcxml->documentElement->setNamespace($MARC_NAMESPACE);
139         __remove_empty_marc_nodes($marcxml);
140         return $marcxml;
141 }
142
143 # remove empty control fields, subfields, and variable data fields, which
144 # can creep in via less-than-correct imported MARC records or issues
145 # with templates
146 sub __remove_empty_marc_nodes {
147         my $marcxml = shift;
148
149         __remove_if_childless($_) foreach $marcxml->documentElement->getElementsByTagNameNS($MARC_NAMESPACE, 'controlfield');
150         __remove_if_childless($_) foreach $marcxml->documentElement->getElementsByTagNameNS($MARC_NAMESPACE, 'subfield');
151         __remove_if_childless($_) foreach $marcxml->documentElement->getElementsByTagNameNS($MARC_NAMESPACE, 'datafield');
152 }
153
154 sub __remove_if_childless {
155         my $node = shift;
156         my @children = $node->childNodes();
157         my $has_nonblank_children = 0;
158         # can do this more concisely by requiring XML::LibXML >= 1.70 and using nonBlankChildNodes()
159         foreach my $node ($node->childNodes()) {
160                 if ($node->nodeType != XML::LibXML::XML_TEXT_NODE || $node->nodeValue !~ /^\s*$/) {
161                         $has_nonblank_children = 1;
162                         last;
163                 }
164         }
165         $node->parentNode->removeChild($node) unless $has_nonblank_children;
166 }
167
168 sub _find_tcn_info { 
169         my $editor              = shift;
170         my $xml                 = shift;
171         my $override    = shift;
172         my $existing_rec        = shift || 0;
173
174         # parse the XML
175         my $marcxml = __make_marc_doc($xml);
176
177         my $xpath = '//marc:controlfield[@tag="001"]';
178         my $tcn = $marcxml->documentElement->findvalue($xpath);
179         $logger->info("biblio import located 001 (tcn) value of $tcn");
180
181         $xpath = '//marc:controlfield[@tag="003"]';
182         my $tcn_source = $marcxml->documentElement->findvalue($xpath) || "System Local";
183
184         if(my $rec = _tcn_exists($editor, $tcn, $tcn_source, $existing_rec) ) {
185
186                 my $origtcn = $tcn;
187                 $tcn = find_free_tcn( $marcxml, $editor, $existing_rec );
188
189                 # if we're overriding, try to find a different TCN to use
190                 if( $override ) {
191
192          # XXX Create ALLOW_ALT_TCN permission check support 
193
194                         $logger->info("tcn value $tcn already exists, attempting to override");
195
196                         if(!$tcn) {
197                                 return ( 
198                                         undef, 
199                                         undef, 
200                                         undef,
201                                         OpenILS::Event->new(
202                                                 'OPEN_TCN_NOT_FOUND', 
203                                                         payload => $marcxml->toString())
204                                         );
205                         }
206
207                 } else {
208
209                         $logger->warn("tcn value $origtcn already exists in import/create");
210
211                         # otherwise, return event
212                         return ( 
213                                 undef, 
214                                 undef, 
215                                 undef,
216                                 OpenILS::Event->new( 
217                                         'TCN_EXISTS', payload => { 
218                                                 dup_record      => $rec, 
219                                                 tcn                     => $origtcn,
220                                                 new_tcn         => $tcn
221                                                 }
222                                         )
223                                 );
224                 }
225         }
226
227         return ($tcn, $tcn_source, $marcxml);
228 }
229
230 sub find_free_tcn {
231
232         my $marcxml = shift;
233         my $editor = shift;
234         my $existing_rec = shift;
235
236         my $xpath = '//marc:datafield[@tag="901"]/marc:subfield[@code="a"]';
237         my ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
238
239     if (!$tcn) {
240             $xpath = '//marc:datafield[@tag="039"]/marc:subfield[@code="a"]';
241             ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
242     }
243
244         $xpath = '//marc:datafield[@tag="901"]/marc:subfield[@code="b"]';
245         my ($tcn_source) = $marcxml->documentElement->findvalue($xpath);
246     if (!$tcn_source) {
247             $xpath = '//marc:datafield[@tag="039"]/marc:subfield[@code="b"]';
248         $tcn_source = $marcxml->documentElement->findvalue($xpath) || "System Local";
249     }
250
251         if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {
252                 $tcn = undef;
253         }
254
255
256         if(!$tcn) {
257                 $xpath = '//marc:datafield[@tag="020"]/marc:subfield[@code="a"]';
258                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
259                 $tcn_source = "ISBN";
260                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
261         }
262
263         if(!$tcn) { 
264                 $xpath = '//marc:datafield[@tag="022"]/marc:subfield[@code="a"]';
265                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
266                 $tcn_source = "ISSN";
267                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
268         }
269
270         if(!$tcn) {
271                 $xpath = '//marc:datafield[@tag="010"]';
272                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
273                 $tcn_source = "LCCN";
274                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
275         }
276
277         if(!$tcn) {
278                 $xpath = '//marc:datafield[@tag="035"]/marc:subfield[@code="a"]';
279                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
280                 $tcn_source = "System Legacy";
281                 if(_tcn_exists($editor, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
282
283                 if($tcn) {
284                         $marcxml->documentElement->removeChild(
285                                 $marcxml->documentElement->findnodes( '//marc:datafield[@tag="035"]' )
286                         );
287                 }
288         }
289
290         return undef unless $tcn;
291         return $tcn;
292 }
293
294
295
296 sub _tcn_exists {
297         my $editor = shift;
298         my $tcn = shift;
299         my $source = shift;
300         my $existing_rec = shift || 0;
301
302         if(!$tcn) {return 0;}
303
304         $logger->debug("tcn_exists search for tcn $tcn and source $source and id $existing_rec");
305
306         # XXX why does the source matter?
307 #       my $req = $session->request(      
308 #               { tcn_value => $tcn, tcn_source => $source, deleted => 'f' } );
309
310     my $recs = $editor->search_biblio_record_entry(
311         {tcn_value => $tcn, deleted => 'f', id => {'!=' => $existing_rec}}, {idlist =>1});
312
313         if(@$recs) {
314                 $logger->debug("_tcn_exists is true for tcn : $tcn ($source)");
315                 return $recs->[0];
316         }
317
318         $logger->debug("_tcn_exists is false for tcn : $tcn ($source)");
319         return 0;
320 }
321
322
323 sub delete_rec {
324    my($class, $editor, $rec_id ) = @_;
325
326    my $rec = $editor->retrieve_biblio_record_entry($rec_id)
327       or return $editor->event;
328
329    return undef if $U->is_true($rec->deleted);
330    
331    $rec->deleted('t');
332    $rec->active('f');
333    $rec->editor( $editor->requestor->id );
334    $rec->edit_date('now');
335
336    # Set the leader/05 to indicate that the record has been deleted
337    my $marc = $rec->marc();
338    $marc =~ s{(<leader>.{5}).}{$1d};
339    $rec->marc($marc);
340
341    $editor->update_biblio_record_entry($rec) or return $editor->event;
342
343     my $holds = $editor->search_action_hold_request({
344         target => $rec->id,
345         hold_type => 'T',
346         cancel_time => undef,
347         fulfillment_time => undef
348     });
349
350     for my $hold (@$holds) {
351
352         $hold->cancel_time('now');
353         $hold->cancel_cause(1); # un-targeted expiration.
354         $editor->update_action_hold_request($hold) or return $editor->die_event;
355
356         my $maps = $editor->search_action_hold_copy_map({hold => $hold->id});
357         for(@$maps) {
358             $editor->delete_action_hold_copy_map($_) 
359                 or return $editor->die_event;
360         }
361
362         my $at_ses = OpenSRF::AppSession->create('open-ils.trigger');
363         $at_ses->request(
364             'open-ils.trigger.event.autocreate',
365             'hold_request.cancel.expire_no_target', 
366             $hold, $hold->pickup_lib);
367     }
368
369    return undef;
370 }
371
372
373 # ---------------------------------------------------------------------------
374 # returns true if the given title (id) has no un-deleted volumes or 
375 # copies attached.  If a context volume is defined, a record
376 # is considered empty only if the context volume is the only
377 # remaining volume on the record.  
378 # ---------------------------------------------------------------------------
379 sub title_is_empty {
380         my($class, $editor, $rid, $vol_id) = @_;
381
382         return 0 if $rid == OILS_PRECAT_RECORD;
383
384         my $cnlist = $editor->search_asset_call_number(
385                 { record => $rid, deleted => 'f' }, { idlist => 1 } );
386
387         return 1 unless @$cnlist; # no attached volumes
388     return 0 if @$cnlist > 1; # multiple attached volumes
389     return 0 unless $$cnlist[0] == $vol_id; # attached volume is not the context vol.
390
391     # see if the sole remaining context volume has any attached copies
392         for my $cn (@$cnlist) {
393                 my $copylist = $editor->search_asset_copy(
394                         [
395                                 { call_number => $cn, deleted => 'f' }, 
396                                 { limit => 1 },
397                         ], { idlist => 1 });
398                 return 0 if @$copylist; # false if we find any copies
399         }
400
401         return 1;
402 }
403 1;