3 # Copyright Biblibre 2008
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 use CGI qw
/:standard -oldstyle_urls/;
30 eval { require PerlIO::gzip };
34 unless ( C4::Context->preference('OAI-PMH') ) {
37 -type => 'text/plain; charset=utf-8',
39 -status => '404 OAI-PMH service is disabled',
41 "OAI-PMH service is disabled";
45 my @encodings = http('HTTP_ACCEPT_ENCODING');
46 if ( $GZIP && grep { defined($_) && $_ eq 'gzip' } @encodings ) {
48 -type => 'text/xml; charset=utf-8',
50 -Content-Encoding => 'gzip',
52 binmode( STDOUT, ":gzip" );
56 -type => 'text/xml; charset=utf-8',
61 binmode STDOUT, ':encoding(UTF-8)';
62 my $repository = C4::OAI::Repository->new();
68 # Extends HTTP::OAI::ResumptionToken
69 # A token is identified by:
75 package C4::OAI::ResumptionToken;
81 use base ("HTTP::OAI::ResumptionToken");
85 my ($class, %args) = @_;
87 my $self = $class->SUPER::new(%args);
89 my ($metadata_prefix, $offset, $from, $until, $set);
90 if ( $args{ resumptionToken } ) {
91 ($metadata_prefix, $offset, $from, $until, $set)
92 = split( ':', $args{resumptionToken} );
95 $metadata_prefix = $args{ metadataPrefix };
96 $from = $args{ from } || '1970-01-01';
97 $until = $args{ until };
99 my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time );
100 $until = sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday );
102 $offset = $args{ offset } || 0;
106 $self->{ metadata_prefix } = $metadata_prefix;
107 $self->{ offset } = $offset;
108 $self->{ from } = $from;
109 $self->{ until } = $until;
110 $self->{ set } = $set;
112 $self->resumptionToken(
113 join( ':', $metadata_prefix, $offset, $from, $until, $set ) );
114 $self->cursor( $offset );
119 # __END__ C4::OAI::ResumptionToken
123 package C4::OAI::Identify;
130 use base ("HTTP::OAI::Identify");
133 my ($class, $repository) = @_;
135 my ($baseURL) = $repository->self_url() =~ /(.*)\?.*/;
136 my $self = $class->SUPER::new(
138 repositoryName => C4::Context->preference("LibraryName"),
139 adminEmail => C4::Context->preference("KohaAdminEmailAddress"),
140 MaxCount => C4::Context->preference("OAI-PMH:MaxCount"),
141 granularity => 'YYYY-MM-DD',
142 earliestDatestamp => '0001-01-01',
143 deletedRecord => 'no',
146 # FIXME - alas, the description element is not so simple; to validate
147 # against the OAI-PMH schema, it cannot contain just a string,
148 # but one or more elements that validate against another XML schema.
149 # For now, simply omitting it.
150 # $self->description( "Koha OAI Repository" );
152 $self->compression( 'gzip' );
157 # __END__ C4::OAI::Identify
161 package C4::OAI::ListMetadataFormats;
167 use base ("HTTP::OAI::ListMetadataFormats");
170 my ($class, $repository) = @_;
172 my $self = $class->SUPER::new();
174 if ( $repository->{ conf } ) {
175 foreach my $name ( @{ $repository->{ koha_metadata_format } } ) {
176 my $format = $repository->{ conf }->{ format }->{ $name };
177 $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
178 metadataPrefix => $format->{metadataPrefix},
179 schema => $format->{schema},
180 metadataNamespace => $format->{metadataNamespace}, ) );
184 $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
185 metadataPrefix => 'oai_dc',
186 schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd',
187 metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/'
189 $self->metadataFormat( HTTP::OAI::MetadataFormat->new(
190 metadataPrefix => 'marcxml',
191 schema => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd',
192 metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim'
199 # __END__ C4::OAI::ListMetadataFormats
203 package C4::OAI::Record;
208 use HTTP::OAI::Metadata::OAI_DC;
210 use base ("HTTP::OAI::Record");
213 my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_;
215 my $self = $class->SUPER::new(%args);
217 $timestamp =~ s/ /T/, $timestamp .= 'Z';
218 $self->header( new HTTP::OAI::Header(
219 identifier => $args{identifier},
220 datestamp => $timestamp,
223 foreach my $setSpec (@$setSpecs) {
224 $self->header->setSpec($setSpec);
227 my $parser = XML::LibXML->new();
228 my $record_dom = $parser->parse_string( $marcxml );
229 my $format = $args{metadataPrefix};
230 if ( $format ne 'marcxml' ) {
232 OPACBaseURL => "'" . C4::Context->preference('OPACBaseURL') . "'"
234 $record_dom = $repository->stylesheet($format)->transform($record_dom, %args);
236 $self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) );
241 # __END__ C4::OAI::Record
245 package C4::OAI::GetRecord;
252 use base ("HTTP::OAI::GetRecord");
256 my ($class, $repository, %args) = @_;
258 my $self = HTTP::OAI::GetRecord->new(%args);
260 my $dbh = C4::Context->dbh;
261 my $sth = $dbh->prepare("
262 SELECT marcxml, timestamp
264 WHERE biblionumber=? " );
265 my $prefix = $repository->{koha_identifier} . ':';
266 my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/;
267 $sth->execute( $biblionumber );
268 my ($marcxml, $timestamp);
269 unless ( ($marcxml, $timestamp) = $sth->fetchrow ) {
270 return HTTP::OAI::Response->new(
271 requestURL => $repository->self_url(),
272 errors => [ new HTTP::OAI::Error(
273 code => 'idDoesNotExist',
274 message => "There is no biblio record with this identifier",
279 my $oai_sets = GetOAISetsBiblio($biblionumber);
281 foreach (@$oai_sets) {
282 push @setSpecs, $_->{spec};
285 #$self->header( HTTP::OAI::Header->new( identifier => $args{identifier} ) );
286 $self->record( C4::OAI::Record->new(
287 $repository, $marcxml, $timestamp, \@setSpecs, %args ) );
292 # __END__ C4::OAI::GetRecord
296 package C4::OAI::ListIdentifiers;
303 use base ("HTTP::OAI::ListIdentifiers");
307 my ($class, $repository, %args) = @_;
309 my $self = HTTP::OAI::ListIdentifiers->new(%args);
311 my $token = new C4::OAI::ResumptionToken( %args );
312 my $dbh = C4::Context->dbh;
314 if(defined $token->{'set'}) {
315 $set = GetOAISetBySpec($token->{'set'});
318 SELECT biblioitems.biblionumber, biblioitems.timestamp
321 $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
322 $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? ";
323 $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
325 LIMIT $repository->{'koha_max_count'}
326 OFFSET $token->{'offset'}
328 my $sth = $dbh->prepare( $sql );
329 my @bind_params = ($token->{'from'}, $token->{'until'});
330 push @bind_params, $set->{'id'} if defined $set;
331 $sth->execute( @bind_params );
333 my $pos = $token->{offset};
334 while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) {
335 $timestamp =~ s/ /T/, $timestamp .= 'Z';
336 $self->identifier( new HTTP::OAI::Header(
337 identifier => $repository->{ koha_identifier} . ':' . $biblionumber,
338 datestamp => $timestamp,
342 $self->resumptionToken(
343 new C4::OAI::ResumptionToken(
344 metadataPrefix => $token->{metadata_prefix},
345 from => $token->{from},
346 until => $token->{until},
350 ) if ($pos > $token->{offset});
355 # __END__ C4::OAI::ListIdentifiers
357 package C4::OAI::Description;
362 use HTTP::OAI::SAXHandler qw/ :SAX /;
365 my ( $class, %args ) = @_;
369 if(my $setDescription = $args{setDescription}) {
370 $self->{setDescription} = $setDescription;
372 if(my $handler = $args{handler}) {
373 $self->{handler} = $handler;
381 my ( $self, $handler ) = @_;
383 $self->{handler} = $handler if $handler;
391 g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription});
396 # __END__ C4::OAI::Description
398 package C4::OAI::ListSets;
405 use base ("HTTP::OAI::ListSets");
408 my ( $class, $repository, %args ) = @_;
410 my $self = HTTP::OAI::ListSets->new(%args);
412 my $token = C4::OAI::ResumptionToken->new(%args);
413 my $sets = GetOAISets;
415 foreach my $set (@$sets) {
416 if ($pos < $token->{offset}) {
421 foreach my $desc (@{$set->{'descriptions'}}) {
422 push @descriptions, C4::OAI::Description->new(
423 setDescription => $desc,
428 setSpec => $set->{'spec'},
429 setName => $set->{'name'},
430 setDescription => \@descriptions,
434 last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count};
437 $self->resumptionToken(
438 new C4::OAI::ResumptionToken(
439 metadataPrefix => $token->{metadata_prefix},
442 ) if ( $pos > $token->{offset} );
447 # __END__ C4::OAI::ListSets;
449 package C4::OAI::ListRecords;
456 use base ("HTTP::OAI::ListRecords");
460 my ($class, $repository, %args) = @_;
462 my $self = HTTP::OAI::ListRecords->new(%args);
464 my $token = new C4::OAI::ResumptionToken( %args );
465 my $dbh = C4::Context->dbh;
467 if(defined $token->{'set'}) {
468 $set = GetOAISetBySpec($token->{'set'});
471 SELECT biblioitems.biblionumber, biblioitems.marcxml, biblioitems.timestamp
474 $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set;
475 $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? ";
476 $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
478 LIMIT $repository->{'koha_max_count'}
479 OFFSET $token->{'offset'}
482 my $sth = $dbh->prepare( $sql );
483 my @bind_params = ($token->{'from'}, $token->{'until'});
484 push @bind_params, $set->{'id'} if defined $set;
485 $sth->execute( @bind_params );
487 my $pos = $token->{offset};
488 while ( my ($biblionumber, $marcxml, $timestamp) = $sth->fetchrow ) {
489 my $oai_sets = GetOAISetsBiblio($biblionumber);
491 foreach (@$oai_sets) {
492 push @setSpecs, $_->{spec};
494 $self->record( C4::OAI::Record->new(
495 $repository, $marcxml, $timestamp, \@setSpecs,
496 identifier => $repository->{ koha_identifier } . ':' . $biblionumber,
497 metadataPrefix => $token->{metadata_prefix}
501 $self->resumptionToken(
502 new C4::OAI::ResumptionToken(
503 metadataPrefix => $token->{metadata_prefix},
504 from => $token->{from},
505 until => $token->{until},
509 ) if ($pos > $token->{offset});
514 # __END__ C4::OAI::ListRecords
518 package C4::OAI::Repository;
520 use base ("HTTP::OAI::Repository");
526 use HTTP::OAI::Repository qw/:validate/;
528 use XML::SAX::Writer;
531 use YAML::Syck qw( LoadFile );
532 use CGI qw
/:standard -oldstyle_urls/;
539 my ($class, %args) = @_;
540 my $self = $class->SUPER::new
(%args);
542 $self->{ koha_identifier
} = C4
::Context
->preference("OAI-PMH:archiveID");
543 $self->{ koha_max_count
} = C4
::Context
->preference("OAI-PMH:MaxCount");
544 $self->{ koha_metadata_format
} = ['oai_dc', 'marcxml'];
545 $self->{ koha_stylesheet
} = { }; # Build when needed
547 # Load configuration file if defined in OAI-PMH:ConfFile syspref
548 if ( my $file = C4
::Context
->preference("OAI-PMH:ConfFile") ) {
549 $self->{ conf
} = LoadFile
( $file );
550 my @formats = keys %{ $self->{conf
}->{format
} };
551 $self->{ koha_metadata_format
} = \
@formats;
554 # Check for grammatical errors in the request
555 my @errs = validate_request
( CGI
::Vars
() );
557 # Is metadataPrefix supported by the respository?
558 my $mdp = param
('metadataPrefix') || '';
559 if ( $mdp && !grep { $_ eq $mdp } @
{$self->{ koha_metadata_format
}} ) {
560 push @errs, new HTTP
::OAI
::Error
(
561 code
=> 'cannotDisseminateFormat',
562 message
=> "Dissemination as '$mdp' is not supported",
568 $response = HTTP
::OAI
::Response
->new(
569 requestURL
=> self_url
(),
574 my %attr = CGI
::Vars
();
575 my $verb = delete( $attr{verb
} );
576 if ( $verb eq 'ListSets' ) {
577 $response = C4
::OAI
::ListSets
->new($self, %attr);
579 elsif ( $verb eq 'Identify' ) {
580 $response = C4
::OAI
::Identify
->new( $self );
582 elsif ( $verb eq 'ListMetadataFormats' ) {
583 $response = C4
::OAI
::ListMetadataFormats
->new( $self );
585 elsif ( $verb eq 'GetRecord' ) {
586 $response = C4
::OAI
::GetRecord
->new( $self, %attr );
588 elsif ( $verb eq 'ListRecords' ) {
589 $response = C4
::OAI
::ListRecords
->new( $self, %attr );
591 elsif ( $verb eq 'ListIdentifiers' ) {
592 $response = C4
::OAI
::ListIdentifiers
->new( $self, %attr );
596 $response->set_handler( XML
::SAX
::Writer
->new( Output
=> *STDOUT
) );
605 my ( $self, $format ) = @_;
607 my $stylesheet = $self->{ koha_stylesheet
}->{ $format };
608 unless ( $stylesheet ) {
609 my $xsl_file = $self->{ conf
}
610 ?
$self->{ conf
}->{ format
}->{ $format }->{ xsl_file
}
611 : ( C4
::Context
->config('intrahtdocs') .
613 C4
::Context
->preference('marcflavour') .
615 my $parser = XML
::LibXML
->new();
616 my $xslt = XML
::LibXSLT
->new();
617 my $style_doc = $parser->parse_file( $xsl_file );
618 $stylesheet = $xslt->parse_stylesheet( $style_doc );
619 $self->{ koha_stylesheet
}->{ $format } = $stylesheet;
629 C4::OAI::Repository - Handles OAI-PMH requests for a Koha database.
633 use C4::OAI::Repository;
635 my $repository = C4::OAI::Repository->new();
639 This object extend HTTP::OAI::Repository object.
640 It accepts OAI-PMH HTTP requests and returns result.
642 This OAI-PMH server can operate in a simple mode and extended one.
644 In simple mode, repository configuration comes entirely from Koha system
645 preferences (OAI-PMH:archiveID and OAI-PMH:MaxCount) and the server returns
646 records in marcxml or dublin core format. Dublin core records are created from
647 koha marcxml records tranformed with XSLT. Used XSL file is located in
648 koha-tmpl/intranet-tmpl/prog/en/xslt directory and choosed based on marcflavour,
649 respecively MARC21slim2OAIDC.xsl for MARC21 and MARC21slim2OAIDC.xsl for
652 In extende mode, it's possible to parameter other format than marcxml or Dublin
653 Core. A new syspref OAI-PMH:ConfFile specify a YAML configuration file which
654 list available metadata formats and XSL file used to create them from marcxml
655 records. If this syspref isn't set, Koha OAI server works in simple mode. A
656 configuration file koha-oai.conf can look like that:
662 metadataNamespace: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs
663 schema: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs.xsd
664 xsl_file: /usr/local/koha/xslt/vs.xsl
666 metadataPrefix: marxml
667 metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim
668 schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd
670 metadataPrefix: oai_dc
671 metadataNamespace: http://www.openarchives.org/OAI/2.0/oai_dc/
672 schema: http://www.openarchives.org/OAI/2.0/oai_dc.xsd
673 xsl_file: /usr/local/koha/koha-tmpl/intranet-tmpl/xslt/UNIMARCslim2OAIDC.xsl