Line 0
Link Here
|
|
|
1 |
package Koha::OAI::Client::Record; |
2 |
|
3 |
# Copyright 2016 Prosentient Systems |
4 |
# |
5 |
# This file is part of Koha. |
6 |
# |
7 |
# Koha is free software; you can redistribute it and/or modify it |
8 |
# under the terms of the GNU General Public License as published by |
9 |
# the Free Software Foundation; either version 3 of the License, or |
10 |
# (at your option) any later version. |
11 |
# |
12 |
# Koha is distributed in the hope that it will be useful, but |
13 |
# WITHOUT ANY WARRANTY; without even the implied warranty of |
14 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 |
# GNU General Public License for more details. |
16 |
# |
17 |
# You should have received a copy of the GNU General Public License |
18 |
# along with Koha; if not, see <http://www.gnu.org/licenses>. |
19 |
# |
20 |
|
21 |
use Modern::Perl; |
22 |
use XML::LibXML; |
23 |
use XML::LibXSLT; |
24 |
use MARC::Record; |
25 |
use URI; |
26 |
use File::Basename; |
27 |
|
28 |
use C4::Context; |
29 |
use C4::Biblio; |
30 |
use C4::ImportBatch; |
31 |
use C4::Matcher; |
32 |
|
33 |
use constant MAX_MATCHES => 99999; #NOTE: This is an arbitrary value. We want to get all matches. |
34 |
|
35 |
sub new { |
36 |
my ($class, $args) = @_; |
37 |
$args = {} unless defined $args; |
38 |
|
39 |
if (my $inxml = $args->{xml_string}){ |
40 |
|
41 |
#Parse the XML string into a XML::LibXML object |
42 |
my $doc = XML::LibXML->load_xml(string => $inxml, { no_blanks => 1 }); |
43 |
#NOTE: Don't load blank nodes |
44 |
$args->{doc} = $doc; |
45 |
|
46 |
#Get the root element |
47 |
my $root = $doc->documentElement; |
48 |
|
49 |
#Register namespaces for searching purposes |
50 |
my $xpc = XML::LibXML::XPathContext->new(); |
51 |
$xpc->registerNs('oai','http://www.openarchives.org/OAI/2.0/'); |
52 |
|
53 |
my $xpath_identifier = XML::LibXML::XPathExpression->new("oai:header/oai:identifier"); |
54 |
my $identifier = $xpc->findnodes($xpath_identifier,$root)->shift; |
55 |
$args->{header_identifier} = $identifier->textContent; |
56 |
|
57 |
my $xpath_datestamp = XML::LibXML::XPathExpression->new("oai:header/oai:datestamp"); |
58 |
my $datestamp = $xpc->findnodes($xpath_datestamp,$root)->shift; |
59 |
$args->{header_datestamp} = $datestamp->textContent; |
60 |
|
61 |
my $xpath_status = XML::LibXML::XPathExpression->new(q{oai:header/@status}); |
62 |
my $status_node = $xpc->findnodes($xpath_status,$root)->shift; |
63 |
$args->{header_status} = $status_node ? $status_node->textContent : ""; |
64 |
} |
65 |
|
66 |
return bless ($args, $class); |
67 |
} |
68 |
|
69 |
sub is_deleted_upstream { |
70 |
my ($self, $args) = @_; |
71 |
if ($self->{header_status}){ |
72 |
if ($self->{header_status} eq "deleted"){ |
73 |
return 1; |
74 |
} |
75 |
} |
76 |
return 0; |
77 |
} |
78 |
|
79 |
sub set_filter { |
80 |
my ($self, $filter_definition) = @_; |
81 |
|
82 |
#Source a default XSLT to use for filtering |
83 |
my $htdocs = C4::Context->config('intrahtdocs'); |
84 |
my $theme = C4::Context->preference("template"); |
85 |
#FIXME: This doesn't work for UNIMARC, as it's specific to MARC21! |
86 |
$self->{filter} = "$htdocs/$theme/en/xslt/OAI2MARC21slim.xsl"; |
87 |
|
88 |
if ($filter_definition){ |
89 |
my ($filter_type, $filter) = $self->_parse_filter($filter_definition); |
90 |
if ($filter_type eq "xslt"){ |
91 |
if ( -f $filter ){ |
92 |
$self->{filter} = $filter; |
93 |
$self->{filter_type} = "xslt"; |
94 |
} |
95 |
} |
96 |
} |
97 |
} |
98 |
|
99 |
sub _parse_filter { |
100 |
my ($self,$filter_definition) = @_; |
101 |
my ($type,$filter); |
102 |
my $filter_uri = URI->new($filter_definition); |
103 |
if ($filter_uri){ |
104 |
my $scheme = $filter_uri->scheme; |
105 |
if ( ($scheme && $scheme eq "file") || ! $scheme ){ |
106 |
my $path = $filter_uri->path; |
107 |
#Filters may theoretically be .xsl or .pm files |
108 |
my($filename, $dirs, $suffix) = fileparse($path,(".xsl",".pm")); |
109 |
if ($suffix){ |
110 |
if ( $suffix eq ".xsl"){ |
111 |
$type = "xslt"; |
112 |
$filter = $path; |
113 |
} |
114 |
} |
115 |
} |
116 |
} |
117 |
return ($type,$filter); |
118 |
} |
119 |
|
120 |
sub filter { |
121 |
my ($self) = @_; |
122 |
my $filtered = 0; |
123 |
my $doc = $self->{doc}; |
124 |
my $filter = $self->{filter}; |
125 |
my $filter_type = $self->{filter_type}; |
126 |
if ($doc){ |
127 |
if ($filter && -f $filter){ |
128 |
if ($filter_type){ |
129 |
if ( $filter_type eq 'xslt' ){ |
130 |
my $xslt = XML::LibXSLT->new(); |
131 |
my $style_doc = XML::LibXML->load_xml(location => $filter); |
132 |
my $stylesheet = $xslt->parse_stylesheet($style_doc); |
133 |
if ($stylesheet){ |
134 |
my $results = $stylesheet->transform($doc); |
135 |
my $filtered_record = $stylesheet->output_as_bytes($results); |
136 |
if ($filtered_record){ |
137 |
$self->{filtered_record} = $filtered_record; |
138 |
return 1; |
139 |
} |
140 |
} |
141 |
} |
142 |
} |
143 |
} |
144 |
} |
145 |
return 0; |
146 |
} |
147 |
|
148 |
sub import_record { |
149 |
my ($self, $args) = @_; |
150 |
my $koha_record_numbers = ""; |
151 |
my $errors = []; |
152 |
my $import_status = "error"; |
153 |
my $match_status = "no_match"; |
154 |
|
155 |
my $batch_id = $args->{import_batch_id}; |
156 |
$self->{import_batch_id} = $batch_id; |
157 |
|
158 |
my $matcher = $args->{matcher}; |
159 |
my $framework = $args->{framework}; |
160 |
|
161 |
my $metadata_xml = $self->{filtered_record}; |
162 |
|
163 |
if ($metadata_xml){ |
164 |
#Convert MARCXML into MARC::Record object |
165 |
my $marcflavour = C4::Context->preference('marcflavour') || 'MARC21'; |
166 |
my $marc_record = eval {MARC::Record::new_from_xml( $metadata_xml, "utf8", $marcflavour)}; |
167 |
if ($@) { |
168 |
push(@$errors, { type => 'create_failed', error_msg => "Error converting OAI-PMH filtered metadata into MARC::Record object: $@", record_id => "", }) ; |
169 |
} |
170 |
|
171 |
if ($self->is_deleted_upstream){ |
172 |
|
173 |
#FIXME: Biblio only. Add authority record support later... |
174 |
my $import_record_id = AddBiblioToBatch($batch_id, 0, $marc_record, "utf8", int(rand(99999))); |
175 |
|
176 |
my $number_of_matches = BatchFindDuplicates($batch_id, $matcher, MAX_MATCHES); |
177 |
if ($number_of_matches > 0){ |
178 |
$match_status = "auto_match"; #See `import_records` table for other options... but this should be the right one. |
179 |
} |
180 |
my $results = GetImportRecordMatches($import_record_id); #Only works for biblio... |
181 |
my $delete_error; |
182 |
|
183 |
my @result_record_numbers = (); |
184 |
foreach my $result (@$results){ |
185 |
if (my $record_id = $result->{biblionumber}){ |
186 |
push(@result_record_numbers,$record_id); |
187 |
|
188 |
#FIXME: Biblio only. Add authority record support later... |
189 |
my $error = C4::Biblio::DelBiblio($record_id); |
190 |
if ($error){ |
191 |
$delete_error++; |
192 |
push(@$errors, { type => 'delete_failed', error_msg => $error, record_id => $record_id, }) ; |
193 |
} |
194 |
} |
195 |
} |
196 |
$koha_record_numbers = join(",",@result_record_numbers); |
197 |
|
198 |
if ($delete_error){ |
199 |
$import_status = "error"; |
200 |
C4::ImportBatch::SetImportBatchStatus($batch_id, 'importing'); |
201 |
} else { |
202 |
$import_status = "ok"; |
203 |
#Ideally, it would be nice to say what records were deleted via the MARC import functionality, but Koha doesn't have that capacity at the moment, so just clean the batch. |
204 |
CleanBatch($batch_id); |
205 |
} |
206 |
|
207 |
} else { |
208 |
#Import the MARCXML record into Koha |
209 |
|
210 |
#FIXME: Biblio only. Add authority record support later... |
211 |
my $import_record_id = AddBiblioToBatch($batch_id, 0, $marc_record, "utf8", int(rand(99999))); |
212 |
|
213 |
#NOTE: Don't provide item imports since there's no reliable way of tracking changes to them over time. |
214 |
#my @import_items_ids = AddItemsToImportBiblio($batch_id, $import_record_id, $marc_record, 'UPDATE COUNTS'); |
215 |
my $number_of_matches = BatchFindDuplicates($batch_id, $matcher); |
216 |
|
217 |
BatchCommitRecords($batch_id, $framework); |
218 |
|
219 |
my $dbh = C4::Context->dbh(); |
220 |
|
221 |
#FIXME: Biblio only. Add authority record support later... |
222 |
my $sth = $dbh->prepare("SELECT matched_biblionumber FROM import_biblios WHERE import_record_id =?"); |
223 |
$sth->execute($import_record_id); |
224 |
$koha_record_numbers = $sth->fetchrow_arrayref->[0] || ''; |
225 |
|
226 |
$sth = $dbh->prepare("SELECT overlay_status FROM import_records WHERE import_record_id =?"); |
227 |
$sth->execute($import_record_id); |
228 |
$match_status = $sth->fetchrow_arrayref->[0] || 'no_match'; |
229 |
|
230 |
$import_status = "ok"; |
231 |
} |
232 |
} else { |
233 |
#There's no filtered metadata... |
234 |
#Clean the batch, so future imports don't use the same batch. |
235 |
CleanBatch($batch_id); |
236 |
} |
237 |
$self->{status} = $import_status; |
238 |
|
239 |
return ($import_status,$match_status,$koha_record_numbers, $errors); |
240 |
} |
241 |
|
242 |
sub save_to_database { |
243 |
my ($self,$args) = @_; |
244 |
my $header_identifier = $self->{header_identifier}; |
245 |
my $header_datestamp = $self->{header_datestamp}; |
246 |
my $header_status = $self->{header_status}; |
247 |
my $record = $self->{doc}->toString(1); |
248 |
my $import_batch_id = $self->{import_batch_id}; |
249 |
my $filter = $self->{filter}; |
250 |
my $status = $self->{status}; |
251 |
my $dbh = C4::Context->dbh; |
252 |
my $sql = "INSERT INTO import_oai (header_identifier, header_datestamp, header_status, record, import_batch_id, filter, status) VALUES (?, ?, ?, ?, ?, ?, ?)"; |
253 |
my $sth = $dbh->prepare($sql); |
254 |
$sth->execute($header_identifier,$header_datestamp,$header_status,$record, $import_batch_id, $filter, $status); |
255 |
} |
256 |
|
257 |
1; |