|
Lines 1-6
Link Here
|
| 1 |
#!/usr/bin/perl |
1 |
#!/usr/bin/perl |
| 2 |
|
2 |
|
| 3 |
# Copyright Biblibre 2008 |
3 |
# Copyright Tamil s.a.r.l. 2008-2015 |
| 4 |
# |
4 |
# |
| 5 |
# This file is part of Koha. |
5 |
# This file is part of Koha. |
| 6 |
# |
6 |
# |
|
Lines 18-29
Link Here
|
| 18 |
# along with Koha; if not, see <http://www.gnu.org/licenses>. |
18 |
# along with Koha; if not, see <http://www.gnu.org/licenses>. |
| 19 |
|
19 |
|
| 20 |
|
20 |
|
| 21 |
use strict; |
21 |
use Modern::Perl; |
| 22 |
use warnings; |
|
|
| 23 |
|
| 24 |
use CGI qw( :standard -oldstyle_urls -utf8 ); |
22 |
use CGI qw( :standard -oldstyle_urls -utf8 ); |
| 25 |
use vars qw( $GZIP ); |
23 |
use vars qw( $GZIP ); |
| 26 |
use C4::Context; |
24 |
use C4::Context; |
|
|
25 |
use Koha::OAI::Repository; |
| 27 |
|
26 |
|
| 28 |
|
27 |
|
| 29 |
BEGIN { |
28 |
BEGIN { |
|
Lines 59-798
else {
Link Here
|
| 59 |
} |
58 |
} |
| 60 |
|
59 |
|
| 61 |
binmode STDOUT, ':encoding(UTF-8)'; |
60 |
binmode STDOUT, ':encoding(UTF-8)'; |
| 62 |
my $repository = C4::OAI::Repository->new(); |
61 |
my $repository = Koha::OAI::Repository->new(); |
| 63 |
|
|
|
| 64 |
|
| 65 |
|
| 66 |
|
| 67 |
# __END__ Main Prog |
| 68 |
|
| 69 |
|
| 70 |
# |
| 71 |
# Extends HTTP::OAI::ResumptionToken |
| 72 |
# A token is identified by: |
| 73 |
# - metadataPrefix |
| 74 |
# - from |
| 75 |
# - until |
| 76 |
# - offset |
| 77 |
# |
| 78 |
package C4::OAI::ResumptionToken; |
| 79 |
|
| 80 |
use strict; |
| 81 |
use warnings; |
| 82 |
use HTTP::OAI; |
| 83 |
|
| 84 |
use base ("HTTP::OAI::ResumptionToken"); |
| 85 |
|
| 86 |
|
| 87 |
sub new { |
| 88 |
my ($class, %args) = @_; |
| 89 |
|
| 90 |
my $self = $class->SUPER::new(%args); |
| 91 |
|
| 92 |
my ($metadata_prefix, $offset, $from, $until, $set); |
| 93 |
if ( $args{ resumptionToken } ) { |
| 94 |
($metadata_prefix, $offset, $from, $until, $set) |
| 95 |
= split( '/', $args{resumptionToken} ); |
| 96 |
} |
| 97 |
else { |
| 98 |
$metadata_prefix = $args{ metadataPrefix }; |
| 99 |
$from = $args{ from } || '1970-01-01'; |
| 100 |
$until = $args{ until }; |
| 101 |
unless ( $until) { |
| 102 |
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time ); |
| 103 |
$until = sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday ); |
| 104 |
} |
| 105 |
#Add times to the arguments, when necessary, so they correctly match against the DB timestamps |
| 106 |
$from .= 'T00:00:00Z' if length($from) == 10; |
| 107 |
$until .= 'T23:59:59Z' if length($until) == 10; |
| 108 |
$offset = $args{ offset } || 0; |
| 109 |
$set = $args{set} || ''; |
| 110 |
} |
| 111 |
|
| 112 |
$self->{ metadata_prefix } = $metadata_prefix; |
| 113 |
$self->{ offset } = $offset; |
| 114 |
$self->{ from } = $from; |
| 115 |
$self->{ until } = $until; |
| 116 |
$self->{ set } = $set; |
| 117 |
$self->{ from_arg } = _strip_UTC_designators($from); |
| 118 |
$self->{ until_arg } = _strip_UTC_designators($until); |
| 119 |
|
| 120 |
$self->resumptionToken( |
| 121 |
join( '/', $metadata_prefix, $offset, $from, $until, $set ) ); |
| 122 |
$self->cursor( $offset ); |
| 123 |
|
| 124 |
return $self; |
| 125 |
} |
| 126 |
|
| 127 |
sub _strip_UTC_designators { |
| 128 |
my ( $timestamp ) = @_; |
| 129 |
$timestamp =~ s/T/ /g; |
| 130 |
$timestamp =~ s/Z//g; |
| 131 |
return $timestamp; |
| 132 |
} |
| 133 |
|
| 134 |
# __END__ C4::OAI::ResumptionToken |
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
package C4::OAI::Identify; |
| 139 |
|
| 140 |
use strict; |
| 141 |
use warnings; |
| 142 |
use HTTP::OAI; |
| 143 |
use C4::Context; |
| 144 |
|
| 145 |
use base ("HTTP::OAI::Identify"); |
| 146 |
|
| 147 |
sub new { |
| 148 |
my ($class, $repository) = @_; |
| 149 |
|
| 150 |
my ($baseURL) = $repository->self_url() =~ /(.*)\?.*/; |
| 151 |
my $self = $class->SUPER::new( |
| 152 |
baseURL => $baseURL, |
| 153 |
repositoryName => C4::Context->preference("LibraryName"), |
| 154 |
adminEmail => C4::Context->preference("KohaAdminEmailAddress"), |
| 155 |
MaxCount => C4::Context->preference("OAI-PMH:MaxCount"), |
| 156 |
granularity => 'YYYY-MM-DD', |
| 157 |
earliestDatestamp => '0001-01-01', |
| 158 |
deletedRecord => C4::Context->preference("OAI-PMH:DeletedRecord") || 'no', |
| 159 |
); |
| 160 |
|
| 161 |
# FIXME - alas, the description element is not so simple; to validate |
| 162 |
# against the OAI-PMH schema, it cannot contain just a string, |
| 163 |
# but one or more elements that validate against another XML schema. |
| 164 |
# For now, simply omitting it. |
| 165 |
# $self->description( "Koha OAI Repository" ); |
| 166 |
|
| 167 |
$self->compression( 'gzip' ); |
| 168 |
|
| 169 |
return $self; |
| 170 |
} |
| 171 |
|
| 172 |
# __END__ C4::OAI::Identify |
| 173 |
|
| 174 |
|
| 175 |
|
| 176 |
package C4::OAI::ListMetadataFormats; |
| 177 |
|
| 178 |
use strict; |
| 179 |
use warnings; |
| 180 |
use HTTP::OAI; |
| 181 |
|
| 182 |
use base ("HTTP::OAI::ListMetadataFormats"); |
| 183 |
|
| 184 |
sub new { |
| 185 |
my ($class, $repository) = @_; |
| 186 |
|
| 187 |
my $self = $class->SUPER::new(); |
| 188 |
|
| 189 |
if ( $repository->{ conf } ) { |
| 190 |
foreach my $name ( @{ $repository->{ koha_metadata_format } } ) { |
| 191 |
my $format = $repository->{ conf }->{ format }->{ $name }; |
| 192 |
$self->metadataFormat( HTTP::OAI::MetadataFormat->new( |
| 193 |
metadataPrefix => $format->{metadataPrefix}, |
| 194 |
schema => $format->{schema}, |
| 195 |
metadataNamespace => $format->{metadataNamespace}, ) ); |
| 196 |
} |
| 197 |
} |
| 198 |
else { |
| 199 |
$self->metadataFormat( HTTP::OAI::MetadataFormat->new( |
| 200 |
metadataPrefix => 'oai_dc', |
| 201 |
schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', |
| 202 |
metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/' |
| 203 |
) ); |
| 204 |
$self->metadataFormat( HTTP::OAI::MetadataFormat->new( |
| 205 |
metadataPrefix => 'marcxml', |
| 206 |
schema => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd', |
| 207 |
metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim' |
| 208 |
) ); |
| 209 |
} |
| 210 |
|
| 211 |
return $self; |
| 212 |
} |
| 213 |
|
| 214 |
# __END__ C4::OAI::ListMetadataFormats |
| 215 |
|
| 216 |
|
| 217 |
|
| 218 |
package C4::OAI::Record; |
| 219 |
|
| 220 |
use strict; |
| 221 |
use warnings; |
| 222 |
use HTTP::OAI; |
| 223 |
use HTTP::OAI::Metadata::OAI_DC; |
| 224 |
|
| 225 |
use base ("HTTP::OAI::Record"); |
| 226 |
|
| 227 |
sub new { |
| 228 |
my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_; |
| 229 |
|
| 230 |
my $self = $class->SUPER::new(%args); |
| 231 |
|
| 232 |
$timestamp =~ s/ /T/, $timestamp .= 'Z'; |
| 233 |
$self->header( new HTTP::OAI::Header( |
| 234 |
identifier => $args{identifier}, |
| 235 |
datestamp => $timestamp, |
| 236 |
) ); |
| 237 |
|
| 238 |
foreach my $setSpec (@$setSpecs) { |
| 239 |
$self->header->setSpec($setSpec); |
| 240 |
} |
| 241 |
|
| 242 |
my $parser = XML::LibXML->new(); |
| 243 |
my $record_dom = $parser->parse_string( $marcxml ); |
| 244 |
my $format = $args{metadataPrefix}; |
| 245 |
if ( $format ne 'marcxml' ) { |
| 246 |
my %args = ( |
| 247 |
OPACBaseURL => "'" . C4::Context->preference('OPACBaseURL') . "'" |
| 248 |
); |
| 249 |
$record_dom = $repository->stylesheet($format)->transform($record_dom, %args); |
| 250 |
} |
| 251 |
$self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) ); |
| 252 |
|
| 253 |
return $self; |
| 254 |
} |
| 255 |
|
| 256 |
# __END__ C4::OAI::Record |
| 257 |
|
| 258 |
package C4::OAI::DeletedRecord; |
| 259 |
|
| 260 |
use Modern::Perl; |
| 261 |
use HTTP::OAI; |
| 262 |
use HTTP::OAI::Metadata::OAI_DC; |
| 263 |
|
| 264 |
use base ("HTTP::OAI::Record"); |
| 265 |
|
| 266 |
sub new { |
| 267 |
my ($class, $timestamp, $setSpecs, %args) = @_; |
| 268 |
|
| 269 |
my $self = $class->SUPER::new(%args); |
| 270 |
|
| 271 |
$timestamp =~ s/ /T/, $timestamp .= 'Z'; |
| 272 |
$self->header( new HTTP::OAI::Header( |
| 273 |
status => 'deleted', |
| 274 |
identifier => $args{identifier}, |
| 275 |
datestamp => $timestamp, |
| 276 |
) ); |
| 277 |
|
| 278 |
foreach my $setSpec (@$setSpecs) { |
| 279 |
$self->header->setSpec($setSpec); |
| 280 |
} |
| 281 |
|
| 282 |
return $self; |
| 283 |
} |
| 284 |
|
| 285 |
# __END__ C4::OAI::DeletedRecord |
| 286 |
|
| 287 |
|
| 288 |
|
| 289 |
package C4::OAI::GetRecord; |
| 290 |
|
| 291 |
use strict; |
| 292 |
use warnings; |
| 293 |
use HTTP::OAI; |
| 294 |
use C4::Biblio; |
| 295 |
use C4::OAI::Sets; |
| 296 |
use MARC::File::XML; |
| 297 |
|
| 298 |
use base ("HTTP::OAI::GetRecord"); |
| 299 |
|
| 300 |
|
| 301 |
sub new { |
| 302 |
my ($class, $repository, %args) = @_; |
| 303 |
|
| 304 |
my $self = HTTP::OAI::GetRecord->new(%args); |
| 305 |
|
| 306 |
my $dbh = C4::Context->dbh; |
| 307 |
my $sth = $dbh->prepare(" |
| 308 |
SELECT timestamp |
| 309 |
FROM biblioitems |
| 310 |
WHERE biblionumber=? " ); |
| 311 |
my $prefix = $repository->{koha_identifier} . ':'; |
| 312 |
my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/; |
| 313 |
$sth->execute( $biblionumber ); |
| 314 |
my ($timestamp, $deleted); |
| 315 |
unless ( ($timestamp) = $sth->fetchrow ) { |
| 316 |
unless ( ($timestamp) = $dbh->selectrow_array(q/ |
| 317 |
SELECT timestamp |
| 318 |
FROM deletedbiblio |
| 319 |
WHERE biblionumber=? /, undef, $biblionumber )) |
| 320 |
{ |
| 321 |
return HTTP::OAI::Response->new( |
| 322 |
requestURL => $repository->self_url(), |
| 323 |
errors => [ new HTTP::OAI::Error( |
| 324 |
code => 'idDoesNotExist', |
| 325 |
message => "There is no biblio record with this identifier", |
| 326 |
) ], |
| 327 |
); |
| 328 |
} |
| 329 |
else { |
| 330 |
$deleted = 1; |
| 331 |
} |
| 332 |
} |
| 333 |
|
| 334 |
# We fetch it using this method, rather than the database directly, |
| 335 |
# so it'll include the item data |
| 336 |
my $marcxml; |
| 337 |
$marcxml = $repository->get_biblio_marcxml($biblionumber, $args{metadataPrefix}) |
| 338 |
unless $deleted; |
| 339 |
my $oai_sets = GetOAISetsBiblio($biblionumber); |
| 340 |
my @setSpecs; |
| 341 |
foreach (@$oai_sets) { |
| 342 |
push @setSpecs, $_->{spec}; |
| 343 |
} |
| 344 |
|
| 345 |
#$self->header( HTTP::OAI::Header->new( identifier => $args{identifier} ) ); |
| 346 |
$self->record( |
| 347 |
$deleted |
| 348 |
? C4::OAI::DeletedRecord->new($timestamp, \@setSpecs, %args) |
| 349 |
: C4::OAI::Record->new($repository, $marcxml, $timestamp, \@setSpecs, %args) |
| 350 |
); |
| 351 |
return $self; |
| 352 |
} |
| 353 |
|
| 354 |
# __END__ C4::OAI::GetRecord |
| 355 |
|
| 356 |
|
| 357 |
|
| 358 |
package C4::OAI::ListIdentifiers; |
| 359 |
|
| 360 |
use strict; |
| 361 |
use warnings; |
| 362 |
use HTTP::OAI; |
| 363 |
use C4::OAI::Sets; |
| 364 |
|
| 365 |
use base ("HTTP::OAI::ListIdentifiers"); |
| 366 |
|
| 367 |
|
| 368 |
sub new { |
| 369 |
my ($class, $repository, %args) = @_; |
| 370 |
|
| 371 |
my $self = HTTP::OAI::ListIdentifiers->new(%args); |
| 372 |
|
| 373 |
my $token = new C4::OAI::ResumptionToken( %args ); |
| 374 |
my $dbh = C4::Context->dbh; |
| 375 |
my $set; |
| 376 |
if(defined $token->{'set'}) { |
| 377 |
$set = GetOAISetBySpec($token->{'set'}); |
| 378 |
} |
| 379 |
my $max = $repository->{koha_max_count}; |
| 380 |
my $sql = " |
| 381 |
(SELECT biblioitems.biblionumber, biblioitems.timestamp |
| 382 |
FROM biblioitems |
| 383 |
"; |
| 384 |
$sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set; |
| 385 |
$sql .= " WHERE timestamp >= ? AND timestamp <= ? "; |
| 386 |
$sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; |
| 387 |
$sql .= ") UNION |
| 388 |
(SELECT deletedbiblio.biblionumber, timestamp FROM deletedbiblio"; |
| 389 |
$sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set; |
| 390 |
$sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? "; |
| 391 |
$sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; |
| 392 |
|
| 393 |
$sql .= ") ORDER BY biblionumber |
| 394 |
LIMIT " . ($max+1) . " |
| 395 |
OFFSET $token->{offset} |
| 396 |
"; |
| 397 |
my $sth = $dbh->prepare( $sql ); |
| 398 |
my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'}); |
| 399 |
push @bind_params, $set->{'id'} if defined $set; |
| 400 |
push @bind_params, ($token->{'from'}, $token->{'until'}); |
| 401 |
push @bind_params, $set->{'id'} if defined $set; |
| 402 |
$sth->execute( @bind_params ); |
| 403 |
|
| 404 |
my $count = 0; |
| 405 |
while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) { |
| 406 |
$count++; |
| 407 |
if ( $count > $max ) { |
| 408 |
$self->resumptionToken( |
| 409 |
new C4::OAI::ResumptionToken( |
| 410 |
metadataPrefix => $token->{metadata_prefix}, |
| 411 |
from => $token->{from}, |
| 412 |
until => $token->{until}, |
| 413 |
offset => $token->{offset} + $max, |
| 414 |
set => $token->{set} |
| 415 |
) |
| 416 |
); |
| 417 |
last; |
| 418 |
} |
| 419 |
$timestamp =~ s/ /T/, $timestamp .= 'Z'; |
| 420 |
$self->identifier( new HTTP::OAI::Header( |
| 421 |
identifier => $repository->{ koha_identifier} . ':' . $biblionumber, |
| 422 |
datestamp => $timestamp, |
| 423 |
) ); |
| 424 |
} |
| 425 |
|
| 426 |
# Return error if no results |
| 427 |
unless ($count) { |
| 428 |
return HTTP::OAI::Response->new( |
| 429 |
requestURL => $repository->self_url(), |
| 430 |
errors => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ], |
| 431 |
); |
| 432 |
} |
| 433 |
|
| 434 |
return $self; |
| 435 |
} |
| 436 |
|
| 437 |
# __END__ C4::OAI::ListIdentifiers |
| 438 |
|
| 439 |
package C4::OAI::Description; |
| 440 |
|
| 441 |
use strict; |
| 442 |
use warnings; |
| 443 |
use HTTP::OAI; |
| 444 |
use HTTP::OAI::SAXHandler qw/ :SAX /; |
| 445 |
|
| 446 |
sub new { |
| 447 |
my ( $class, %args ) = @_; |
| 448 |
|
| 449 |
my $self = {}; |
| 450 |
|
| 451 |
if(my $setDescription = $args{setDescription}) { |
| 452 |
$self->{setDescription} = $setDescription; |
| 453 |
} |
| 454 |
if(my $handler = $args{handler}) { |
| 455 |
$self->{handler} = $handler; |
| 456 |
} |
| 457 |
|
| 458 |
bless $self, $class; |
| 459 |
return $self; |
| 460 |
} |
| 461 |
|
| 462 |
sub set_handler { |
| 463 |
my ( $self, $handler ) = @_; |
| 464 |
|
| 465 |
$self->{handler} = $handler if $handler; |
| 466 |
|
| 467 |
return $self; |
| 468 |
} |
| 469 |
|
| 470 |
sub generate { |
| 471 |
my ( $self ) = @_; |
| 472 |
|
| 473 |
g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription}); |
| 474 |
|
| 475 |
return $self; |
| 476 |
} |
| 477 |
|
| 478 |
# __END__ C4::OAI::Description |
| 479 |
|
| 480 |
package C4::OAI::ListSets; |
| 481 |
|
| 482 |
use strict; |
| 483 |
use warnings; |
| 484 |
use HTTP::OAI; |
| 485 |
use C4::OAI::Sets; |
| 486 |
|
| 487 |
use base ("HTTP::OAI::ListSets"); |
| 488 |
|
| 489 |
sub new { |
| 490 |
my ( $class, $repository, %args ) = @_; |
| 491 |
|
| 492 |
my $self = HTTP::OAI::ListSets->new(%args); |
| 493 |
|
| 494 |
my $token = C4::OAI::ResumptionToken->new(%args); |
| 495 |
my $sets = GetOAISets; |
| 496 |
my $pos = 0; |
| 497 |
foreach my $set (@$sets) { |
| 498 |
if ($pos < $token->{offset}) { |
| 499 |
$pos++; |
| 500 |
next; |
| 501 |
} |
| 502 |
my @descriptions; |
| 503 |
foreach my $desc (@{$set->{'descriptions'}}) { |
| 504 |
push @descriptions, C4::OAI::Description->new( |
| 505 |
setDescription => $desc, |
| 506 |
); |
| 507 |
} |
| 508 |
$self->set( |
| 509 |
HTTP::OAI::Set->new( |
| 510 |
setSpec => $set->{'spec'}, |
| 511 |
setName => $set->{'name'}, |
| 512 |
setDescription => \@descriptions, |
| 513 |
) |
| 514 |
); |
| 515 |
$pos++; |
| 516 |
last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count}; |
| 517 |
} |
| 518 |
|
| 519 |
$self->resumptionToken( |
| 520 |
new C4::OAI::ResumptionToken( |
| 521 |
metadataPrefix => $token->{metadata_prefix}, |
| 522 |
offset => $pos |
| 523 |
) |
| 524 |
) if ( $pos > $token->{offset} ); |
| 525 |
|
| 526 |
return $self; |
| 527 |
} |
| 528 |
|
| 529 |
# __END__ C4::OAI::ListSets; |
| 530 |
|
| 531 |
package C4::OAI::ListRecords; |
| 532 |
|
| 533 |
use strict; |
| 534 |
use warnings; |
| 535 |
use C4::Biblio; |
| 536 |
use HTTP::OAI; |
| 537 |
use C4::OAI::Sets; |
| 538 |
use MARC::File::XML; |
| 539 |
|
| 540 |
use base ("HTTP::OAI::ListRecords"); |
| 541 |
|
| 542 |
|
| 543 |
sub new { |
| 544 |
my ($class, $repository, %args) = @_; |
| 545 |
|
| 546 |
my $self = HTTP::OAI::ListRecords->new(%args); |
| 547 |
|
| 548 |
my $token = new C4::OAI::ResumptionToken( %args ); |
| 549 |
my $dbh = C4::Context->dbh; |
| 550 |
my $set; |
| 551 |
if(defined $token->{'set'}) { |
| 552 |
$set = GetOAISetBySpec($token->{'set'}); |
| 553 |
} |
| 554 |
my $max = $repository->{koha_max_count}; |
| 555 |
my $sql = " |
| 556 |
(SELECT biblioitems.biblionumber, biblioitems.timestamp, marcxml |
| 557 |
FROM biblioitems |
| 558 |
"; |
| 559 |
$sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set; |
| 560 |
$sql .= " WHERE timestamp >= ? AND timestamp <= ? "; |
| 561 |
$sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; |
| 562 |
$sql .= ") UNION |
| 563 |
(SELECT deletedbiblio.biblionumber, null as marcxml, timestamp FROM deletedbiblio"; |
| 564 |
$sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set; |
| 565 |
$sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? "; |
| 566 |
$sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; |
| 567 |
|
| 568 |
$sql .= ") ORDER BY biblionumber |
| 569 |
LIMIT " . ($max + 1) . " |
| 570 |
OFFSET $token->{offset} |
| 571 |
"; |
| 572 |
my $sth = $dbh->prepare( $sql ); |
| 573 |
my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'}); |
| 574 |
push @bind_params, $set->{'id'} if defined $set; |
| 575 |
push @bind_params, ($token->{'from'}, $token->{'until'}); |
| 576 |
push @bind_params, $set->{'id'} if defined $set; |
| 577 |
$sth->execute( @bind_params ); |
| 578 |
|
| 579 |
my $count = 0; |
| 580 |
while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) { |
| 581 |
$count++; |
| 582 |
if ( $count > $max ) { |
| 583 |
$self->resumptionToken( |
| 584 |
new C4::OAI::ResumptionToken( |
| 585 |
metadataPrefix => $token->{metadata_prefix}, |
| 586 |
from => $token->{from}, |
| 587 |
until => $token->{until}, |
| 588 |
offset => $token->{offset} + $max, |
| 589 |
set => $token->{set} |
| 590 |
) |
| 591 |
); |
| 592 |
last; |
| 593 |
} |
| 594 |
my $marcxml = $repository->get_biblio_marcxml($biblionumber, $args{metadataPrefix}); |
| 595 |
my $oai_sets = GetOAISetsBiblio($biblionumber); |
| 596 |
my @setSpecs; |
| 597 |
foreach (@$oai_sets) { |
| 598 |
push @setSpecs, $_->{spec}; |
| 599 |
} |
| 600 |
if ($marcxml) { |
| 601 |
$self->record( C4::OAI::Record->new( |
| 602 |
$repository, $marcxml, $timestamp, \@setSpecs, |
| 603 |
identifier => $repository->{ koha_identifier } . ':' . $biblionumber, |
| 604 |
metadataPrefix => $token->{metadata_prefix} |
| 605 |
) ); |
| 606 |
} else { |
| 607 |
$self->record( C4::OAI::DeletedRecord->new( |
| 608 |
$timestamp, \@setSpecs, identifier => $repository->{ koha_identifier } . ':' . $biblionumber ) ); |
| 609 |
} |
| 610 |
} |
| 611 |
|
| 612 |
# Return error if no results |
| 613 |
unless ($count) { |
| 614 |
return HTTP::OAI::Response->new( |
| 615 |
requestURL => $repository->self_url(), |
| 616 |
errors => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ], |
| 617 |
); |
| 618 |
} |
| 619 |
|
| 620 |
return $self; |
| 621 |
} |
| 622 |
|
| 623 |
# __END__ C4::OAI::ListRecords |
| 624 |
|
| 625 |
|
| 626 |
|
| 627 |
package C4::OAI::Repository; |
| 628 |
|
| 629 |
use base ("HTTP::OAI::Repository"); |
| 630 |
|
| 631 |
use strict; |
| 632 |
use warnings; |
| 633 |
|
| 634 |
use HTTP::OAI; |
| 635 |
use HTTP::OAI::Repository qw/:validate/; |
| 636 |
|
| 637 |
use XML::SAX::Writer; |
| 638 |
use XML::LibXML; |
| 639 |
use XML::LibXSLT; |
| 640 |
use YAML::Syck qw( LoadFile ); |
| 641 |
use CGI qw/:standard -oldstyle_urls/; |
| 642 |
|
| 643 |
use C4::Context; |
| 644 |
use C4::Biblio; |
| 645 |
|
| 646 |
|
| 647 |
sub new { |
| 648 |
my ($class, %args) = @_; |
| 649 |
my $self = $class->SUPER::new(%args); |
| 650 |
|
| 651 |
$self->{ koha_identifier } = C4::Context->preference("OAI-PMH:archiveID"); |
| 652 |
$self->{ koha_max_count } = C4::Context->preference("OAI-PMH:MaxCount"); |
| 653 |
$self->{ koha_metadata_format } = ['oai_dc', 'marcxml']; |
| 654 |
$self->{ koha_stylesheet } = { }; # Build when needed |
| 655 |
|
| 656 |
# Load configuration file if defined in OAI-PMH:ConfFile syspref |
| 657 |
if ( my $file = C4::Context->preference("OAI-PMH:ConfFile") ) { |
| 658 |
$self->{ conf } = LoadFile( $file ); |
| 659 |
my @formats = keys %{ $self->{conf}->{format} }; |
| 660 |
$self->{ koha_metadata_format } = \@formats; |
| 661 |
} |
| 662 |
|
| 663 |
# Check for grammatical errors in the request |
| 664 |
my @errs = validate_request( CGI::Vars() ); |
| 665 |
|
| 666 |
# Is metadataPrefix supported by the respository? |
| 667 |
my $mdp = param('metadataPrefix') || ''; |
| 668 |
if ( $mdp && !grep { $_ eq $mdp } @{$self->{ koha_metadata_format }} ) { |
| 669 |
push @errs, new HTTP::OAI::Error( |
| 670 |
code => 'cannotDisseminateFormat', |
| 671 |
message => "Dissemination as '$mdp' is not supported", |
| 672 |
); |
| 673 |
} |
| 674 |
|
| 675 |
my $response; |
| 676 |
if ( @errs ) { |
| 677 |
$response = HTTP::OAI::Response->new( |
| 678 |
requestURL => self_url(), |
| 679 |
errors => \@errs, |
| 680 |
); |
| 681 |
} |
| 682 |
else { |
| 683 |
my %attr = CGI::Vars(); |
| 684 |
my $verb = delete( $attr{verb} ); |
| 685 |
if ( $verb eq 'ListSets' ) { |
| 686 |
$response = C4::OAI::ListSets->new($self, %attr); |
| 687 |
} |
| 688 |
elsif ( $verb eq 'Identify' ) { |
| 689 |
$response = C4::OAI::Identify->new( $self ); |
| 690 |
} |
| 691 |
elsif ( $verb eq 'ListMetadataFormats' ) { |
| 692 |
$response = C4::OAI::ListMetadataFormats->new( $self ); |
| 693 |
} |
| 694 |
elsif ( $verb eq 'GetRecord' ) { |
| 695 |
$response = C4::OAI::GetRecord->new( $self, %attr ); |
| 696 |
} |
| 697 |
elsif ( $verb eq 'ListRecords' ) { |
| 698 |
$response = C4::OAI::ListRecords->new( $self, %attr ); |
| 699 |
} |
| 700 |
elsif ( $verb eq 'ListIdentifiers' ) { |
| 701 |
$response = C4::OAI::ListIdentifiers->new( $self, %attr ); |
| 702 |
} |
| 703 |
} |
| 704 |
|
| 705 |
$response->set_handler( XML::SAX::Writer->new( Output => *STDOUT ) ); |
| 706 |
$response->generate; |
| 707 |
|
| 708 |
bless $self, $class; |
| 709 |
return $self; |
| 710 |
} |
| 711 |
|
| 712 |
|
| 713 |
sub get_biblio_marcxml { |
| 714 |
my ($self, $biblionumber, $format) = @_; |
| 715 |
my $with_items = 0; |
| 716 |
if ( my $conf = $self->{conf} ) { |
| 717 |
$with_items = $conf->{format}->{$format}->{include_items}; |
| 718 |
} |
| 719 |
my $record = GetMarcBiblio($biblionumber, $with_items, 1); |
| 720 |
$record ? $record->as_xml() : undef; |
| 721 |
} |
| 722 |
|
| 723 |
|
| 724 |
sub stylesheet { |
| 725 |
my ( $self, $format ) = @_; |
| 726 |
|
| 727 |
my $stylesheet = $self->{ koha_stylesheet }->{ $format }; |
| 728 |
unless ( $stylesheet ) { |
| 729 |
my $xsl_file = $self->{ conf } |
| 730 |
? $self->{ conf }->{ format }->{ $format }->{ xsl_file } |
| 731 |
: ( C4::Context->config('intrahtdocs') . |
| 732 |
'/prog/en/xslt/' . |
| 733 |
C4::Context->preference('marcflavour') . |
| 734 |
'slim2OAIDC.xsl' ); |
| 735 |
my $parser = XML::LibXML->new(); |
| 736 |
my $xslt = XML::LibXSLT->new(); |
| 737 |
my $style_doc = $parser->parse_file( $xsl_file ); |
| 738 |
$stylesheet = $xslt->parse_stylesheet( $style_doc ); |
| 739 |
$self->{ koha_stylesheet }->{ $format } = $stylesheet; |
| 740 |
} |
| 741 |
|
| 742 |
return $stylesheet; |
| 743 |
} |
| 744 |
|
| 745 |
|
| 746 |
|
| 747 |
=head1 NAME |
| 748 |
|
| 749 |
C4::OAI::Repository - Handles OAI-PMH requests for a Koha database. |
| 750 |
|
| 751 |
=head1 SYNOPSIS |
| 752 |
|
| 753 |
use C4::OAI::Repository; |
| 754 |
|
| 755 |
my $repository = C4::OAI::Repository->new(); |
| 756 |
|
| 757 |
=head1 DESCRIPTION |
| 758 |
|
| 759 |
This object extend HTTP::OAI::Repository object. |
| 760 |
It accepts OAI-PMH HTTP requests and returns result. |
| 761 |
|
| 762 |
This OAI-PMH server can operate in a simple mode and extended one. |
| 763 |
|
| 764 |
In simple mode, repository configuration comes entirely from Koha system |
| 765 |
preferences (OAI-PMH:archiveID and OAI-PMH:MaxCount) and the server returns |
| 766 |
records in marcxml or dublin core format. Dublin core records are created from |
| 767 |
koha marcxml records tranformed with XSLT. Used XSL file is located in |
| 768 |
koha-tmpl/intranet-tmpl/prog/en/xslt directory and choosed based on marcflavour, |
| 769 |
respecively MARC21slim2OAIDC.xsl for MARC21 and MARC21slim2OAIDC.xsl for |
| 770 |
UNIMARC. |
| 771 |
|
| 772 |
In extende mode, it's possible to parameter other format than marcxml or Dublin |
| 773 |
Core. A new syspref OAI-PMH:ConfFile specify a YAML configuration file which |
| 774 |
list available metadata formats and XSL file used to create them from marcxml |
| 775 |
records. If this syspref isn't set, Koha OAI server works in simple mode. A |
| 776 |
configuration file koha-oai.conf can look like that: |
| 777 |
|
| 778 |
--- |
| 779 |
format: |
| 780 |
vs: |
| 781 |
metadataPrefix: vs |
| 782 |
metadataNamespace: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs |
| 783 |
schema: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs.xsd |
| 784 |
xsl_file: /usr/local/koha/xslt/vs.xsl |
| 785 |
marcxml: |
| 786 |
metadataPrefix: marxml |
| 787 |
metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim |
| 788 |
schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd |
| 789 |
oai_dc: |
| 790 |
metadataPrefix: oai_dc |
| 791 |
metadataNamespace: http://www.openarchives.org/OAI/2.0/oai_dc/ |
| 792 |
schema: http://www.openarchives.org/OAI/2.0/oai_dc.xsd |
| 793 |
xsl_file: /usr/local/koha/koha-tmpl/intranet-tmpl/xslt/UNIMARCslim2OAIDC.xsl |
| 794 |
|
| 795 |
=cut |
| 796 |
|
| 797 |
|
| 798 |
|