@@ -, +, @@ --- C4/Installer/PerlDependencies.pm | 5 + Koha/OAI/Server/DeletedRecord.pm | 45 ++ Koha/OAI/Server/Description.pm | 59 +++ Koha/OAI/Server/GetRecord.pm | 82 ++++ Koha/OAI/Server/Identify.pm | 51 +++ Koha/OAI/Server/ListIdentifiers.pm | 96 +++++ Koha/OAI/Server/ListMetadataFormats.pm | 56 +++ Koha/OAI/Server/ListRecords.pm | 113 +++++ Koha/OAI/Server/ListSets.pm | 65 +++ Koha/OAI/Server/Record.pm | 56 +++ Koha/OAI/Server/Repository.pm | 176 ++++++++ Koha/OAI/Server/ResumptionToken.pm | 82 ++++ debian/control | 1 + opac/oai.pl | 745 +-------------------------------- t/db_dependent/OAI/Server.t | 120 ++++++ 15 files changed, 1011 insertions(+), 741 deletions(-) create mode 100644 Koha/OAI/Server/DeletedRecord.pm create mode 100644 Koha/OAI/Server/Description.pm create mode 100644 Koha/OAI/Server/GetRecord.pm create mode 100644 Koha/OAI/Server/Identify.pm create mode 100644 Koha/OAI/Server/ListIdentifiers.pm create mode 100644 Koha/OAI/Server/ListMetadataFormats.pm create mode 100644 Koha/OAI/Server/ListRecords.pm create mode 100644 Koha/OAI/Server/ListSets.pm create mode 100644 Koha/OAI/Server/Record.pm create mode 100644 Koha/OAI/Server/Repository.pm create mode 100644 Koha/OAI/Server/ResumptionToken.pm create mode 100644 t/db_dependent/OAI/Server.t --- a/C4/Installer/PerlDependencies.pm +++ a/C4/Installer/PerlDependencies.pm @@ -777,6 +777,11 @@ our $PERL_DEPS = { 'required' => '0', 'min_ver' => '0.28', }, + 'Capture::Tiny' => { + 'usage' => 'Core', + 'required' => '0', + 'min_ver' => '0.18', + }, }; 1; --- a/Koha/OAI/Server/DeletedRecord.pm +++ a/Koha/OAI/Server/DeletedRecord.pm @@ -0,0 +1,45 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::DeletedRecord; + +use Modern::Perl; +use HTTP::OAI; +use HTTP::OAI::Metadata::OAI_DC; + +use base ("HTTP::OAI::Record"); + +sub new { + my ($class, $timestamp, $setSpecs, %args) = @_; + + my $self = $class->SUPER::new(%args); + + $timestamp =~ s/ /T/, $timestamp .= 'Z'; + $self->header( new HTTP::OAI::Header( + status => 'deleted', + identifier => $args{identifier}, + datestamp => $timestamp, + ) ); + + foreach my $setSpec (@$setSpecs) { + $self->header->setSpec($setSpec); + } + + return $self; +} + +1; --- a/Koha/OAI/Server/Description.pm +++ a/Koha/OAI/Server/Description.pm @@ -0,0 +1,59 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::Description; + +use Modern::Perl; +use HTTP::OAI; +use HTTP::OAI::SAXHandler qw/ :SAX /; + + +sub new { + my ( $class, %args ) = @_; + + my $self = {}; + + if(my $setDescription = $args{setDescription}) { + $self->{setDescription} = $setDescription; + } + if(my $handler = $args{handler}) { + $self->{handler} = $handler; + } + + bless $self, $class; + return $self; +} + + +sub set_handler { + my ( $self, $handler ) = @_; + + $self->{handler} = $handler if $handler; + + return $self; +} + + +sub generate { + my ( $self ) = @_; + + g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription}); + + return $self; +} + +1; --- a/Koha/OAI/Server/GetRecord.pm +++ a/Koha/OAI/Server/GetRecord.pm @@ -0,0 +1,82 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::GetRecord; + +use Modern::Perl; +use HTTP::OAI; +use C4::Biblio; +use C4::OAI::Sets; +use MARC::File::XML; + +use base ("HTTP::OAI::GetRecord"); + + +sub new { + my ($class, $repository, %args) = @_; + + my $self = HTTP::OAI::GetRecord->new(%args); + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare(" + SELECT timestamp + FROM biblioitems + WHERE biblionumber=? " ); + my $prefix = $repository->{koha_identifier} . ':'; + my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/; + $sth->execute( $biblionumber ); + my ($timestamp, $deleted); + unless ( ($timestamp) = $sth->fetchrow ) { + unless ( ($timestamp) = $dbh->selectrow_array(q/ + SELECT timestamp + FROM deletedbiblio + WHERE biblionumber=? /, undef, $biblionumber )) + { + return HTTP::OAI::Response->new( + requestURL => $repository->self_url(), + errors => [ new HTTP::OAI::Error( + code => 'idDoesNotExist', + message => "There is no biblio record with this identifier", + ) ], + ); + } + else { + $deleted = 1; + } + } + + # We fetch it using this method, rather than the database directly, + # so it'll include the item data + my $marcxml; + $marcxml = $repository->get_biblio_marcxml($biblionumber, $args{metadataPrefix}) + unless $deleted; + my $oai_sets = GetOAISetsBiblio($biblionumber); + my @setSpecs; + foreach (@$oai_sets) { + push @setSpecs, $_->{spec}; + } + + #$self->header( HTTP::OAI::Header->new( identifier => $args{identifier} ) ); + $self->record( + $deleted + ? Koha::OAI::Server::DeletedRecord->new($timestamp, \@setSpecs, %args) + : Koha::OAI::Server::Record->new($repository, $marcxml, $timestamp, \@setSpecs, %args) + ); + return $self; +} + +1; --- a/Koha/OAI/Server/Identify.pm +++ a/Koha/OAI/Server/Identify.pm @@ -0,0 +1,51 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Identify; + +use Modern::Perl; +use HTTP::OAI; +use C4::Context; + +use base ("HTTP::OAI::Identify"); + +sub new { + my ($class, $repository) = @_; + + my ($baseURL) = $repository->self_url() =~ /(.*)\?.*/; + my $self = $class->SUPER::new( + baseURL => $baseURL, + repositoryName => C4::Context->preference("LibraryName"), + adminEmail => C4::Context->preference("KohaAdminEmailAddress"), + MaxCount => C4::Context->preference("OAI-PMH:MaxCount"), + granularity => 'YYYY-MM-DD', + earliestDatestamp => '0001-01-01', + deletedRecord => C4::Context->preference("OAI-PMH:DeletedRecord") || 'no', + ); + + # FIXME - alas, the description element is not so simple; to validate + # against the OAI-PMH schema, it cannot contain just a string, + # but one or more elements that validate against another XML schema. + # For now, simply omitting it. + # $self->description( "Koha OAI Repository" ); + + $self->compression( 'gzip' ); + + return $self; +} + +1; --- a/Koha/OAI/Server/ListIdentifiers.pm +++ a/Koha/OAI/Server/ListIdentifiers.pm @@ -0,0 +1,96 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::ListIdentifiers; + +use Modern::Perl; +use HTTP::OAI; +use C4::OAI::Sets; + +use base ("HTTP::OAI::ListIdentifiers"); + + +sub new { + my ($class, $repository, %args) = @_; + + my $self = HTTP::OAI::ListIdentifiers->new(%args); + + my $token = new Koha::OAI::Server::ResumptionToken( %args ); + my $dbh = C4::Context->dbh; + my $set; + if(defined $token->{'set'}) { + $set = GetOAISetBySpec($token->{'set'}); + } + my $max = $repository->{koha_max_count}; + my $sql = " + (SELECT biblioitems.biblionumber, biblioitems.timestamp + FROM biblioitems + "; + $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set; + $sql .= " WHERE timestamp >= ? AND timestamp <= ? "; + $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; + $sql .= ") UNION + (SELECT deletedbiblio.biblionumber, timestamp FROM deletedbiblio"; + $sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set; + $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? "; + $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; + + $sql .= ") ORDER BY biblionumber + LIMIT " . ($max+1) . " + OFFSET $token->{offset} + "; + my $sth = $dbh->prepare( $sql ); + my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'}); + push @bind_params, $set->{'id'} if defined $set; + push @bind_params, ($token->{'from'}, $token->{'until'}); + push @bind_params, $set->{'id'} if defined $set; + $sth->execute( @bind_params ); + + my $count = 0; + while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) { + $count++; + if ( $count > $max ) { + $self->resumptionToken( + new Koha::OAI::Server::ResumptionToken( + metadataPrefix => $token->{metadata_prefix}, + from => $token->{from}, + until => $token->{until}, + offset => $token->{offset} + $max, + set => $token->{set} + ) + ); + last; + } + $timestamp =~ s/ /T/, $timestamp .= 'Z'; + $self->identifier( new HTTP::OAI::Header( + identifier => $repository->{ koha_identifier} . ':' . $biblionumber, + datestamp => $timestamp, + ) ); + } + + # Return error if no results + unless ($count) { + return HTTP::OAI::Response->new( + requestURL => $repository->self_url(), + errors => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ], + ); + } + + return $self; +} + +1; --- a/Koha/OAI/Server/ListMetadataFormats.pm +++ a/Koha/OAI/Server/ListMetadataFormats.pm @@ -0,0 +1,56 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::ListMetadataFormats; + +use Modern::Perl; +use HTTP::OAI; + +use base ("HTTP::OAI::ListMetadataFormats"); + + +sub new { + my ($class, $repository) = @_; + + my $self = $class->SUPER::new(); + + if ( $repository->{ conf } ) { + foreach my $name ( @{ $repository->{ koha_metadata_format } } ) { + my $format = $repository->{ conf }->{ format }->{ $name }; + $self->metadataFormat( HTTP::OAI::MetadataFormat->new( + metadataPrefix => $format->{metadataPrefix}, + schema => $format->{schema}, + metadataNamespace => $format->{metadataNamespace}, ) ); + } + } + else { + $self->metadataFormat( HTTP::OAI::MetadataFormat->new( + metadataPrefix => 'oai_dc', + schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', + metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/' + ) ); + $self->metadataFormat( HTTP::OAI::MetadataFormat->new( + metadataPrefix => 'marcxml', + schema => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd', + metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim' + ) ); + } + + return $self; +} + +1; --- a/Koha/OAI/Server/ListRecords.pm +++ a/Koha/OAI/Server/ListRecords.pm @@ -0,0 +1,113 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::ListRecords; + +use Modern::Perl; +use C4::Biblio; +use HTTP::OAI; +use Koha::OAI::Server::ResumptionToken; +use Koha::OAI::Server::Record; +use Koha::OAI::Server::DeletedRecord; +use C4::OAI::Sets; +use MARC::File::XML; + +use base ("HTTP::OAI::ListRecords"); + + +sub new { + my ($class, $repository, %args) = @_; + + my $self = HTTP::OAI::ListRecords->new(%args); + + my $token = new Koha::OAI::Server::ResumptionToken( %args ); + my $dbh = C4::Context->dbh; + my $set; + if(defined $token->{'set'}) { + $set = GetOAISetBySpec($token->{'set'}); + } + my $max = $repository->{koha_max_count}; + my $sql = " + (SELECT biblioitems.biblionumber, biblioitems.timestamp, marcxml + FROM biblioitems + "; + $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set; + $sql .= " WHERE timestamp >= ? AND timestamp <= ? "; + $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; + $sql .= ") UNION + (SELECT deletedbiblio.biblionumber, null as marcxml, timestamp FROM deletedbiblio"; + $sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set; + $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? "; + $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; + + $sql .= ") ORDER BY biblionumber + LIMIT " . ($max + 1) . " + OFFSET $token->{offset} + "; + my $sth = $dbh->prepare( $sql ); + my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'}); + push @bind_params, $set->{'id'} if defined $set; + push @bind_params, ($token->{'from'}, $token->{'until'}); + push @bind_params, $set->{'id'} if defined $set; + $sth->execute( @bind_params ); + + my $count = 0; + my $format = $args{metadataPrefix} || $token->{metadata_prefix}; + while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) { + $count++; + if ( $count > $max ) { + $self->resumptionToken( + new Koha::OAI::Server::ResumptionToken( + metadataPrefix => $token->{metadata_prefix}, + from => $token->{from}, + until => $token->{until}, + offset => $token->{offset} + $max, + set => $token->{set} + ) + ); + last; + } + my $marcxml = $repository->get_biblio_marcxml($biblionumber, $format); + my $oai_sets = GetOAISetsBiblio($biblionumber); + my @setSpecs; + foreach (@$oai_sets) { + push @setSpecs, $_->{spec}; + } + if ($marcxml) { + $self->record( Koha::OAI::Server::Record->new( + $repository, $marcxml, $timestamp, \@setSpecs, + identifier => $repository->{ koha_identifier } . ':' . $biblionumber, + metadataPrefix => $token->{metadata_prefix} + ) ); + } else { + $self->record( Koha::OAI::Server::DeletedRecord->new( + $timestamp, \@setSpecs, identifier => $repository->{ koha_identifier } . ':' . $biblionumber ) ); + } + } + + # Return error if no results + unless ($count) { + return HTTP::OAI::Response->new( + requestURL => $repository->self_url(), + errors => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ], + ); + } + + return $self; +} + +1; --- a/Koha/OAI/Server/ListSets.pm +++ a/Koha/OAI/Server/ListSets.pm @@ -0,0 +1,65 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::ListSets; + +use Modern::Perl; +use HTTP::OAI; +use Koha::OAI::Server::ResumptionToken; +use Koha::OAI::Server::Description; +use C4::OAI::Sets; + +use base ("HTTP::OAI::ListSets"); + + +sub new { + my ( $class, $repository, %args ) = @_; + + my $self = HTTP::OAI::ListSets->new(%args); + my $token = Koha::OAI::Server::ResumptionToken->new(%args); + my $sets = GetOAISets; + my $pos = 0; + foreach my $set (@$sets) { + if ($pos < $token->{offset}) { + $pos++; + next; + } + my @descriptions = map { + Koha::OAI::Server::Description->new( setDescription => $_ ); + } @{$set->{'descriptions'}}; + $self->set( + HTTP::OAI::Set->new( + setSpec => $set->{'spec'}, + setName => $set->{'name'}, + setDescription => \@descriptions, + ) + ); + $pos++; + last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count}; + } + + $self->resumptionToken( + new Koha::OAI::Server::ResumptionToken( + metadataPrefix => $token->{metadata_prefix}, + offset => $pos + ) + ) if ( $pos > $token->{offset} ); + + return $self; +} + +1; --- a/Koha/OAI/Server/Record.pm +++ a/Koha/OAI/Server/Record.pm @@ -0,0 +1,56 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::Record; + +use Modern::Perl; +use HTTP::OAI; +use HTTP::OAI::Metadata::OAI_DC; + +use base ("HTTP::OAI::Record"); + + +sub new { + my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_; + + my $self = $class->SUPER::new(%args); + + $timestamp =~ s/ /T/, $timestamp .= 'Z'; + $self->header( new HTTP::OAI::Header( + identifier => $args{identifier}, + datestamp => $timestamp, + ) ); + + foreach my $setSpec (@$setSpecs) { + $self->header->setSpec($setSpec); + } + + my $parser = XML::LibXML->new(); + my $record_dom = $parser->parse_string( $marcxml ); + my $format = $args{metadataPrefix}; + if ( $format ne 'marcxml' ) { + my %args = ( + OPACBaseURL => "'" . C4::Context->preference('OPACBaseURL') . "'" + ); + $record_dom = $repository->stylesheet($format)->transform($record_dom, %args); + } + $self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) ); + + return $self; +} + +1; --- a/Koha/OAI/Server/Repository.pm +++ a/Koha/OAI/Server/Repository.pm @@ -0,0 +1,176 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +package Koha::OAI::Server::Repository; + +use base ("HTTP::OAI::Repository"); + +use Modern::Perl; +use HTTP::OAI; +use HTTP::OAI::Repository qw/:validate/; +use Koha::OAI::Server::Identify; +use Koha::OAI::Server::ListSets; +use Koha::OAI::Server::ListMetadataFormats; +use Koha::OAI::Server::GetRecord; +use Koha::OAI::Server::ListRecords; +use Koha::OAI::Server::ListIdentifiers; +use XML::SAX::Writer; +use XML::LibXML; +use XML::LibXSLT; +use YAML::Syck qw( LoadFile ); +use CGI qw/:standard -oldstyle_urls/; +use C4::Context; +use C4::Biblio; + + +=head1 NAME + +Koha::OAI::Server::Repository - Handles OAI-PMH requests for a Koha database. + +=head1 SYNOPSIS + + use Koha::OAI::Server::Repository; + + my $repository = Koha::OAI::Server::Repository->new(); + +=head1 DESCRIPTION + +This object extend HTTP::OAI::Repository object. +It accepts OAI-PMH HTTP requests and returns result. + +This OAI-PMH server can operate in a simple mode and extended one. + +In simple mode, repository configuration comes entirely from Koha system +preferences (OAI-PMH:archiveID and OAI-PMH:MaxCount) and the server returns +records in marcxml or dublin core format. Dublin core records are created from +koha marcxml records tranformed with XSLT. Used XSL file is located in koha- +tmpl/intranet-tmpl/prog/en/xslt directory and choosed based on marcflavour, +respecively MARC21slim2OAIDC.xsl for MARC21 and MARC21slim2OAIDC.xsl for +UNIMARC. + +In extended mode, it's possible to parameter other format than marcxml or +Dublin Core. A new syspref OAI-PMH:ConfFile specify a YAML configuration file +which list available metadata formats and XSL file used to create them from +marcxml records. If this syspref isn't set, Koha OAI server works in simple +mode. A configuration file koha-oai.conf can look like that: + + --- + format: + vs: + metadataPrefix: vs + metadataNamespace: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs + schema: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs.xsd + xsl_file: /usr/local/koha/xslt/vs.xsl + marcxml: + metadataPrefix: marxml + metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim + schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd + include_items: 1 + oai_dc: + metadataPrefix: oai_dc + metadataNamespace: http://www.openarchives.org/OAI/2.0/oai_dc/ + schema: http://www.openarchives.org/OAI/2.0/oai_dc.xsd + xsl_file: /usr/local/koha/koha-tmpl/intranet-tmpl/xslt/UNIMARCslim2OAIDC.xsl + +Note de 'include_items' parameter which is the only mean to return item-level info. + +=cut + + +sub new { + my ($class, %args) = @_; + my $self = $class->SUPER::new(%args); + + $self->{ koha_identifier } = C4::Context->preference("OAI-PMH:archiveID"); + $self->{ koha_max_count } = C4::Context->preference("OAI-PMH:MaxCount"); + $self->{ koha_metadata_format } = ['oai_dc', 'marcxml']; + $self->{ koha_stylesheet } = { }; # Build when needed + + # Load configuration file if defined in OAI-PMH:ConfFile syspref + if ( my $file = C4::Context->preference("OAI-PMH:ConfFile") ) { + $self->{ conf } = LoadFile( $file ); + my @formats = keys %{ $self->{conf}->{format} }; + $self->{ koha_metadata_format } = \@formats; + } + + # Check for grammatical errors in the request + my @errs = validate_request( CGI::Vars() ); + + # Is metadataPrefix supported by the respository? + my $mdp = param('metadataPrefix') || ''; + if ( $mdp && !grep { $_ eq $mdp } @{$self->{ koha_metadata_format }} ) { + push @errs, new HTTP::OAI::Error( + code => 'cannotDisseminateFormat', + message => "Dissemination as '$mdp' is not supported", + ); + } + + my $response; + if ( @errs ) { + $response = HTTP::OAI::Response->new( + requestURL => self_url(), + errors => \@errs, + ); + } + else { + my %attr = CGI::Vars(); + my $verb = delete $attr{verb}; + my $class = "Koha::OAI::Server::$verb"; + $response = $class->new($self, %attr); + } + + $response->set_handler( XML::SAX::Writer->new( Output => *STDOUT ) ); + $response->generate; + + bless $self, $class; + return $self; +} + + +sub get_biblio_marcxml { + my ($self, $biblionumber, $format) = @_; + my $with_items = 0; + if ( my $conf = $self->{conf} ) { + $with_items = $conf->{format}->{$format}->{include_items}; + } + my $record = GetMarcBiblio($biblionumber, $with_items, 1); + $record ? $record->as_xml() : undef; +} + + +sub stylesheet { + my ( $self, $format ) = @_; + + my $stylesheet = $self->{ koha_stylesheet }->{ $format }; + unless ( $stylesheet ) { + my $xsl_file = $self->{ conf } + ? $self->{ conf }->{ format }->{ $format }->{ xsl_file } + : ( C4::Context->config('intrahtdocs') . + '/prog/en/xslt/' . + C4::Context->preference('marcflavour') . + 'slim2OAIDC.xsl' ); + my $parser = XML::LibXML->new(); + my $xslt = XML::LibXSLT->new(); + my $style_doc = $parser->parse_file( $xsl_file ); + $stylesheet = $xslt->parse_stylesheet( $style_doc ); + $self->{ koha_stylesheet }->{ $format } = $stylesheet; + } + + return $stylesheet; +} + +1; --- a/Koha/OAI/Server/ResumptionToken.pm +++ a/Koha/OAI/Server/ResumptionToken.pm @@ -0,0 +1,82 @@ +# Copyright Tamil s.a.r.l. 2008-2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + + +package Koha::OAI::Server::ResumptionToken; + +use Modern::Perl; +use HTTP::OAI; + +use base ("HTTP::OAI::ResumptionToken"); + + +# Extends HTTP::OAI::ResumptionToken +# A token is identified by: +# - metadataPrefix +# - from +# - until +# - offset + + +sub new { + my ($class, %args) = @_; + + my $self = $class->SUPER::new(%args); + + my ($metadata_prefix, $offset, $from, $until, $set); + if ( $args{ resumptionToken } ) { + ($metadata_prefix, $offset, $from, $until, $set) + = split( '/', $args{resumptionToken} ); + } + else { + $metadata_prefix = $args{ metadataPrefix }; + $from = $args{ from } || '1970-01-01'; + $until = $args{ until }; + unless ( $until) { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time ); + $until = sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday ); + } + #Add times to the arguments, when necessary, so they correctly match against the DB timestamps + $from .= 'T00:00:00Z' if length($from) == 10; + $until .= 'T23:59:59Z' if length($until) == 10; + $offset = $args{ offset } || 0; + $set = $args{set} || ''; + } + + $self->{ metadata_prefix } = $metadata_prefix; + $self->{ offset } = $offset; + $self->{ from } = $from; + $self->{ until } = $until; + $self->{ set } = $set; + $self->{ from_arg } = _strip_UTC_designators($from); + $self->{ until_arg } = _strip_UTC_designators($until); + + $self->resumptionToken( + join( '/', $metadata_prefix, $offset, $from, $until, $set ) ); + $self->cursor( $offset ); + + return $self; +} + +sub _strip_UTC_designators { + my ( $timestamp ) = @_; + $timestamp =~ s/T/ /g; + $timestamp =~ s/Z//g; + return $timestamp; +} + +1; --- a/debian/control +++ a/debian/control @@ -226,6 +226,7 @@ Depends: libalgorithm-checkdigits-perl, libcache-fastmmap-perl, libcache-memcached-fast-perl, libcache-perl, + libcapture-tiny-perl, libcgi-pm-perl | perl-modules, libcgi-session-driver-memcached-perl, libcgi-session-perl, --- a/opac/oai.pl +++ a/opac/oai.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl -# Copyright Biblibre 2008 +# Copyright Tamil s.a.r.l. 2008-2015 # # This file is part of Koha. # @@ -18,12 +18,11 @@ # along with Koha; if not, see . -use strict; -use warnings; - +use Modern::Perl; use CGI qw( :standard -oldstyle_urls -utf8 ); use vars qw( $GZIP ); use C4::Context; +use Koha::OAI::Server::Repository; BEGIN { @@ -59,741 +58,5 @@ else { } binmode STDOUT, ':encoding(UTF-8)'; -my $repository = C4::OAI::Repository->new(); - - - - -# __END__ Main Prog - - -# -# Extends HTTP::OAI::ResumptionToken -# A token is identified by: -# - metadataPrefix -# - from -# - until -# - offset -# -package C4::OAI::ResumptionToken; - -use strict; -use warnings; -use HTTP::OAI; - -use base ("HTTP::OAI::ResumptionToken"); - - -sub new { - my ($class, %args) = @_; - - my $self = $class->SUPER::new(%args); - - my ($metadata_prefix, $offset, $from, $until, $set); - if ( $args{ resumptionToken } ) { - ($metadata_prefix, $offset, $from, $until, $set) - = split( '/', $args{resumptionToken} ); - } - else { - $metadata_prefix = $args{ metadataPrefix }; - $from = $args{ from } || '1970-01-01'; - $until = $args{ until }; - unless ( $until) { - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time ); - $until = sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday ); - } - #Add times to the arguments, when necessary, so they correctly match against the DB timestamps - $from .= 'T00:00:00Z' if length($from) == 10; - $until .= 'T23:59:59Z' if length($until) == 10; - $offset = $args{ offset } || 0; - $set = $args{set} || ''; - } - - $self->{ metadata_prefix } = $metadata_prefix; - $self->{ offset } = $offset; - $self->{ from } = $from; - $self->{ until } = $until; - $self->{ set } = $set; - $self->{ from_arg } = _strip_UTC_designators($from); - $self->{ until_arg } = _strip_UTC_designators($until); - - $self->resumptionToken( - join( '/', $metadata_prefix, $offset, $from, $until, $set ) ); - $self->cursor( $offset ); - - return $self; -} - -sub _strip_UTC_designators { - my ( $timestamp ) = @_; - $timestamp =~ s/T/ /g; - $timestamp =~ s/Z//g; - return $timestamp; -} - -# __END__ C4::OAI::ResumptionToken - - - -package C4::OAI::Identify; - -use strict; -use warnings; -use HTTP::OAI; -use C4::Context; - -use base ("HTTP::OAI::Identify"); - -sub new { - my ($class, $repository) = @_; - - my ($baseURL) = $repository->self_url() =~ /(.*)\?.*/; - my $self = $class->SUPER::new( - baseURL => $baseURL, - repositoryName => C4::Context->preference("LibraryName"), - adminEmail => C4::Context->preference("KohaAdminEmailAddress"), - MaxCount => C4::Context->preference("OAI-PMH:MaxCount"), - granularity => 'YYYY-MM-DD', - earliestDatestamp => '0001-01-01', - deletedRecord => C4::Context->preference("OAI-PMH:DeletedRecord") || 'no', - ); - - # FIXME - alas, the description element is not so simple; to validate - # against the OAI-PMH schema, it cannot contain just a string, - # but one or more elements that validate against another XML schema. - # For now, simply omitting it. - # $self->description( "Koha OAI Repository" ); - - $self->compression( 'gzip' ); - - return $self; -} - -# __END__ C4::OAI::Identify - - - -package C4::OAI::ListMetadataFormats; - -use strict; -use warnings; -use HTTP::OAI; - -use base ("HTTP::OAI::ListMetadataFormats"); - -sub new { - my ($class, $repository) = @_; - - my $self = $class->SUPER::new(); - - if ( $repository->{ conf } ) { - foreach my $name ( @{ $repository->{ koha_metadata_format } } ) { - my $format = $repository->{ conf }->{ format }->{ $name }; - $self->metadataFormat( HTTP::OAI::MetadataFormat->new( - metadataPrefix => $format->{metadataPrefix}, - schema => $format->{schema}, - metadataNamespace => $format->{metadataNamespace}, ) ); - } - } - else { - $self->metadataFormat( HTTP::OAI::MetadataFormat->new( - metadataPrefix => 'oai_dc', - schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', - metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/' - ) ); - $self->metadataFormat( HTTP::OAI::MetadataFormat->new( - metadataPrefix => 'marcxml', - schema => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd', - metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim' - ) ); - } - - return $self; -} - -# __END__ C4::OAI::ListMetadataFormats - - - -package C4::OAI::Record; - -use strict; -use warnings; -use HTTP::OAI; -use HTTP::OAI::Metadata::OAI_DC; - -use base ("HTTP::OAI::Record"); - -sub new { - my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_; - - my $self = $class->SUPER::new(%args); - - $timestamp =~ s/ /T/, $timestamp .= 'Z'; - $self->header( new HTTP::OAI::Header( - identifier => $args{identifier}, - datestamp => $timestamp, - ) ); - - foreach my $setSpec (@$setSpecs) { - $self->header->setSpec($setSpec); - } - - my $parser = XML::LibXML->new(); - my $record_dom = $parser->parse_string( $marcxml ); - my $format = $args{metadataPrefix}; - if ( $format ne 'marcxml' ) { - my %args = ( - OPACBaseURL => "'" . C4::Context->preference('OPACBaseURL') . "'" - ); - $record_dom = $repository->stylesheet($format)->transform($record_dom, %args); - } - $self->metadata( HTTP::OAI::Metadata->new( dom => $record_dom ) ); - - return $self; -} - -# __END__ C4::OAI::Record - -package C4::OAI::DeletedRecord; - -use Modern::Perl; -use HTTP::OAI; -use HTTP::OAI::Metadata::OAI_DC; - -use base ("HTTP::OAI::Record"); - -sub new { - my ($class, $timestamp, $setSpecs, %args) = @_; - - my $self = $class->SUPER::new(%args); - - $timestamp =~ s/ /T/, $timestamp .= 'Z'; - $self->header( new HTTP::OAI::Header( - status => 'deleted', - identifier => $args{identifier}, - datestamp => $timestamp, - ) ); - - foreach my $setSpec (@$setSpecs) { - $self->header->setSpec($setSpec); - } - - return $self; -} - -# __END__ C4::OAI::DeletedRecord - - - -package C4::OAI::GetRecord; - -use strict; -use warnings; -use HTTP::OAI; -use C4::Biblio; -use C4::OAI::Sets; -use MARC::File::XML; - -use base ("HTTP::OAI::GetRecord"); - - -sub new { - my ($class, $repository, %args) = @_; - - my $self = HTTP::OAI::GetRecord->new(%args); - - my $dbh = C4::Context->dbh; - my $sth = $dbh->prepare(" - SELECT timestamp - FROM biblioitems - WHERE biblionumber=? " ); - my $prefix = $repository->{koha_identifier} . ':'; - my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/; - $sth->execute( $biblionumber ); - my ($timestamp, $deleted); - unless ( ($timestamp) = $sth->fetchrow ) { - unless ( ($timestamp) = $dbh->selectrow_array(q/ - SELECT timestamp - FROM deletedbiblio - WHERE biblionumber=? /, undef, $biblionumber )) - { - return HTTP::OAI::Response->new( - requestURL => $repository->self_url(), - errors => [ new HTTP::OAI::Error( - code => 'idDoesNotExist', - message => "There is no biblio record with this identifier", - ) ], - ); - } - else { - $deleted = 1; - } - } - - # We fetch it using this method, rather than the database directly, - # so it'll include the item data - my $marcxml; - $marcxml = $repository->get_biblio_marcxml($biblionumber, $args{metadataPrefix}) - unless $deleted; - my $oai_sets = GetOAISetsBiblio($biblionumber); - my @setSpecs; - foreach (@$oai_sets) { - push @setSpecs, $_->{spec}; - } - - #$self->header( HTTP::OAI::Header->new( identifier => $args{identifier} ) ); - $self->record( - $deleted - ? C4::OAI::DeletedRecord->new($timestamp, \@setSpecs, %args) - : C4::OAI::Record->new($repository, $marcxml, $timestamp, \@setSpecs, %args) - ); - return $self; -} - -# __END__ C4::OAI::GetRecord - - - -package C4::OAI::ListIdentifiers; - -use strict; -use warnings; -use HTTP::OAI; -use C4::OAI::Sets; - -use base ("HTTP::OAI::ListIdentifiers"); - - -sub new { - my ($class, $repository, %args) = @_; - - my $self = HTTP::OAI::ListIdentifiers->new(%args); - - my $token = new C4::OAI::ResumptionToken( %args ); - my $dbh = C4::Context->dbh; - my $set; - if(defined $token->{'set'}) { - $set = GetOAISetBySpec($token->{'set'}); - } - my $max = $repository->{koha_max_count}; - my $sql = " - (SELECT biblioitems.biblionumber, biblioitems.timestamp - FROM biblioitems - "; - $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set; - $sql .= " WHERE timestamp >= ? AND timestamp <= ? "; - $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; - $sql .= ") UNION - (SELECT deletedbiblio.biblionumber, timestamp FROM deletedbiblio"; - $sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set; - $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? "; - $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; - - $sql .= ") ORDER BY biblionumber - LIMIT " . ($max+1) . " - OFFSET $token->{offset} - "; - my $sth = $dbh->prepare( $sql ); - my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'}); - push @bind_params, $set->{'id'} if defined $set; - push @bind_params, ($token->{'from'}, $token->{'until'}); - push @bind_params, $set->{'id'} if defined $set; - $sth->execute( @bind_params ); - - my $count = 0; - while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) { - $count++; - if ( $count > $max ) { - $self->resumptionToken( - new C4::OAI::ResumptionToken( - metadataPrefix => $token->{metadata_prefix}, - from => $token->{from}, - until => $token->{until}, - offset => $token->{offset} + $max, - set => $token->{set} - ) - ); - last; - } - $timestamp =~ s/ /T/, $timestamp .= 'Z'; - $self->identifier( new HTTP::OAI::Header( - identifier => $repository->{ koha_identifier} . ':' . $biblionumber, - datestamp => $timestamp, - ) ); - } - - # Return error if no results - unless ($count) { - return HTTP::OAI::Response->new( - requestURL => $repository->self_url(), - errors => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ], - ); - } - - return $self; -} - -# __END__ C4::OAI::ListIdentifiers - -package C4::OAI::Description; - -use strict; -use warnings; -use HTTP::OAI; -use HTTP::OAI::SAXHandler qw/ :SAX /; - -sub new { - my ( $class, %args ) = @_; - - my $self = {}; - - if(my $setDescription = $args{setDescription}) { - $self->{setDescription} = $setDescription; - } - if(my $handler = $args{handler}) { - $self->{handler} = $handler; - } - - bless $self, $class; - return $self; -} - -sub set_handler { - my ( $self, $handler ) = @_; - - $self->{handler} = $handler if $handler; - - return $self; -} - -sub generate { - my ( $self ) = @_; - - g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription}); - - return $self; -} - -# __END__ C4::OAI::Description - -package C4::OAI::ListSets; - -use strict; -use warnings; -use HTTP::OAI; -use C4::OAI::Sets; - -use base ("HTTP::OAI::ListSets"); - -sub new { - my ( $class, $repository, %args ) = @_; - - my $self = HTTP::OAI::ListSets->new(%args); - - my $token = C4::OAI::ResumptionToken->new(%args); - my $sets = GetOAISets; - my $pos = 0; - foreach my $set (@$sets) { - if ($pos < $token->{offset}) { - $pos++; - next; - } - my @descriptions; - foreach my $desc (@{$set->{'descriptions'}}) { - push @descriptions, C4::OAI::Description->new( - setDescription => $desc, - ); - } - $self->set( - HTTP::OAI::Set->new( - setSpec => $set->{'spec'}, - setName => $set->{'name'}, - setDescription => \@descriptions, - ) - ); - $pos++; - last if ($pos + 1 - $token->{offset}) > $repository->{koha_max_count}; - } - - $self->resumptionToken( - new C4::OAI::ResumptionToken( - metadataPrefix => $token->{metadata_prefix}, - offset => $pos - ) - ) if ( $pos > $token->{offset} ); - - return $self; -} - -# __END__ C4::OAI::ListSets; - -package C4::OAI::ListRecords; - -use strict; -use warnings; -use C4::Biblio; -use HTTP::OAI; -use C4::OAI::Sets; -use MARC::File::XML; - -use base ("HTTP::OAI::ListRecords"); - - -sub new { - my ($class, $repository, %args) = @_; - - my $self = HTTP::OAI::ListRecords->new(%args); - - my $token = new C4::OAI::ResumptionToken( %args ); - my $dbh = C4::Context->dbh; - my $set; - if(defined $token->{'set'}) { - $set = GetOAISetBySpec($token->{'set'}); - } - my $max = $repository->{koha_max_count}; - my $sql = " - (SELECT biblioitems.biblionumber, biblioitems.timestamp, marcxml - FROM biblioitems - "; - $sql .= " JOIN oai_sets_biblios ON biblioitems.biblionumber = oai_sets_biblios.biblionumber " if defined $set; - $sql .= " WHERE timestamp >= ? AND timestamp <= ? "; - $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; - $sql .= ") UNION - (SELECT deletedbiblio.biblionumber, null as marcxml, timestamp FROM deletedbiblio"; - $sql .= " JOIN oai_sets_biblios ON deletedbiblio.biblionumber = oai_sets_biblios.biblionumber " if defined $set; - $sql .= " WHERE DATE(timestamp) >= ? AND DATE(timestamp) <= ? "; - $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set; - - $sql .= ") ORDER BY biblionumber - LIMIT " . ($max + 1) . " - OFFSET $token->{offset} - "; - my $sth = $dbh->prepare( $sql ); - my @bind_params = ($token->{'from_arg'}, $token->{'until_arg'}); - push @bind_params, $set->{'id'} if defined $set; - push @bind_params, ($token->{'from'}, $token->{'until'}); - push @bind_params, $set->{'id'} if defined $set; - $sth->execute( @bind_params ); - - my $count = 0; - my $format = $args{metadataPrefix} || $token->{metadata_prefix}; - while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) { - $count++; - if ( $count > $max ) { - $self->resumptionToken( - new C4::OAI::ResumptionToken( - metadataPrefix => $token->{metadata_prefix}, - from => $token->{from}, - until => $token->{until}, - offset => $token->{offset} + $max, - set => $token->{set} - ) - ); - last; - } - my $marcxml = $repository->get_biblio_marcxml($biblionumber, $format); - my $oai_sets = GetOAISetsBiblio($biblionumber); - my @setSpecs; - foreach (@$oai_sets) { - push @setSpecs, $_->{spec}; - } - if ($marcxml) { - $self->record( C4::OAI::Record->new( - $repository, $marcxml, $timestamp, \@setSpecs, - identifier => $repository->{ koha_identifier } . ':' . $biblionumber, - metadataPrefix => $token->{metadata_prefix} - ) ); - } else { - $self->record( C4::OAI::DeletedRecord->new( - $timestamp, \@setSpecs, identifier => $repository->{ koha_identifier } . ':' . $biblionumber ) ); - } - } - - # Return error if no results - unless ($count) { - return HTTP::OAI::Response->new( - requestURL => $repository->self_url(), - errors => [ new HTTP::OAI::Error( code => 'noRecordsMatch' ) ], - ); - } - - return $self; -} - -# __END__ C4::OAI::ListRecords - - - -package C4::OAI::Repository; - -use base ("HTTP::OAI::Repository"); - -use strict; -use warnings; - -use HTTP::OAI; -use HTTP::OAI::Repository qw/:validate/; - -use XML::SAX::Writer; -use XML::LibXML; -use XML::LibXSLT; -use YAML::Syck qw( LoadFile ); -use CGI qw/:standard -oldstyle_urls/; - -use C4::Context; -use C4::Biblio; - - -sub new { - my ($class, %args) = @_; - my $self = $class->SUPER::new(%args); - - $self->{ koha_identifier } = C4::Context->preference("OAI-PMH:archiveID"); - $self->{ koha_max_count } = C4::Context->preference("OAI-PMH:MaxCount"); - $self->{ koha_metadata_format } = ['oai_dc', 'marcxml']; - $self->{ koha_stylesheet } = { }; # Build when needed - - # Load configuration file if defined in OAI-PMH:ConfFile syspref - if ( my $file = C4::Context->preference("OAI-PMH:ConfFile") ) { - $self->{ conf } = LoadFile( $file ); - my @formats = keys %{ $self->{conf}->{format} }; - $self->{ koha_metadata_format } = \@formats; - } - - # Check for grammatical errors in the request - my @errs = validate_request( CGI::Vars() ); - - # Is metadataPrefix supported by the respository? - my $mdp = param('metadataPrefix') || ''; - if ( $mdp && !grep { $_ eq $mdp } @{$self->{ koha_metadata_format }} ) { - push @errs, new HTTP::OAI::Error( - code => 'cannotDisseminateFormat', - message => "Dissemination as '$mdp' is not supported", - ); - } - - my $response; - if ( @errs ) { - $response = HTTP::OAI::Response->new( - requestURL => self_url(), - errors => \@errs, - ); - } - else { - my %attr = CGI::Vars(); - my $verb = delete( $attr{verb} ); - if ( $verb eq 'ListSets' ) { - $response = C4::OAI::ListSets->new($self, %attr); - } - elsif ( $verb eq 'Identify' ) { - $response = C4::OAI::Identify->new( $self ); - } - elsif ( $verb eq 'ListMetadataFormats' ) { - $response = C4::OAI::ListMetadataFormats->new( $self ); - } - elsif ( $verb eq 'GetRecord' ) { - $response = C4::OAI::GetRecord->new( $self, %attr ); - } - elsif ( $verb eq 'ListRecords' ) { - $response = C4::OAI::ListRecords->new( $self, %attr ); - } - elsif ( $verb eq 'ListIdentifiers' ) { - $response = C4::OAI::ListIdentifiers->new( $self, %attr ); - } - } - - $response->set_handler( XML::SAX::Writer->new( Output => *STDOUT ) ); - $response->generate; - - bless $self, $class; - return $self; -} - - -sub get_biblio_marcxml { - my ($self, $biblionumber, $format) = @_; - my $with_items = 0; - if ( my $conf = $self->{conf} ) { - $with_items = $conf->{format}->{$format}->{include_items}; - } - my $record = GetMarcBiblio($biblionumber, $with_items, 1); - $record ? $record->as_xml() : undef; -} - - -sub stylesheet { - my ( $self, $format ) = @_; - - my $stylesheet = $self->{ koha_stylesheet }->{ $format }; - unless ( $stylesheet ) { - my $xsl_file = $self->{ conf } - ? $self->{ conf }->{ format }->{ $format }->{ xsl_file } - : ( C4::Context->config('intrahtdocs') . - '/prog/en/xslt/' . - C4::Context->preference('marcflavour') . - 'slim2OAIDC.xsl' ); - my $parser = XML::LibXML->new(); - my $xslt = XML::LibXSLT->new(); - my $style_doc = $parser->parse_file( $xsl_file ); - $stylesheet = $xslt->parse_stylesheet( $style_doc ); - $self->{ koha_stylesheet }->{ $format } = $stylesheet; - } - - return $stylesheet; -} - - - -=head1 NAME - -C4::OAI::Repository - Handles OAI-PMH requests for a Koha database. - -=head1 SYNOPSIS - - use C4::OAI::Repository; - - my $repository = C4::OAI::Repository->new(); - -=head1 DESCRIPTION - -This object extend HTTP::OAI::Repository object. -It accepts OAI-PMH HTTP requests and returns result. - -This OAI-PMH server can operate in a simple mode and extended one. - -In simple mode, repository configuration comes entirely from Koha system -preferences (OAI-PMH:archiveID and OAI-PMH:MaxCount) and the server returns -records in marcxml or dublin core format. Dublin core records are created from -koha marcxml records tranformed with XSLT. Used XSL file is located in -koha-tmpl/intranet-tmpl/prog/en/xslt directory and choosed based on marcflavour, -respecively MARC21slim2OAIDC.xsl for MARC21 and MARC21slim2OAIDC.xsl for -UNIMARC. - -In extende mode, it's possible to parameter other format than marcxml or Dublin -Core. A new syspref OAI-PMH:ConfFile specify a YAML configuration file which -list available metadata formats and XSL file used to create them from marcxml -records. If this syspref isn't set, Koha OAI server works in simple mode. A -configuration file koha-oai.conf can look like that: - - --- - format: - vs: - metadataPrefix: vs - metadataNamespace: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs - schema: http://veryspecial.tamil.fr/vs/format-pivot/1.1/vs.xsd - xsl_file: /usr/local/koha/xslt/vs.xsl - marcxml: - metadataPrefix: marxml - metadataNamespace: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim - schema: http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd - oai_dc: - metadataPrefix: oai_dc - metadataNamespace: http://www.openarchives.org/OAI/2.0/oai_dc/ - schema: http://www.openarchives.org/OAI/2.0/oai_dc.xsd - xsl_file: /usr/local/koha/koha-tmpl/intranet-tmpl/xslt/UNIMARCslim2OAIDC.xsl - -=cut - - +my $repository = Koha::OAI::Server::Repository->new(); --- a/t/db_dependent/OAI/Server.t +++ a/t/db_dependent/OAI/Server.t @@ -0,0 +1,120 @@ +#!/usr/bin/perl + +# Copyright Tamil s.a.r.l. 2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + + +use Modern::Perl; +use C4::Context; +use C4::Biblio; +use Test::More tests => 13; +use Test::MockModule; +use Test::Warn; +use DateTime; +use Capture::Tiny ':all'; +use XML::Simple; +use t::lib::Mocks; + + +BEGIN { + use_ok('Koha::OAI::Server::DeletedRecord'); + use_ok('Koha::OAI::Server::Description'); + use_ok('Koha::OAI::Server::GetRecord'); + use_ok('Koha::OAI::Server::Identify'); + use_ok('Koha::OAI::Server::ListIdentifiers'); + use_ok('Koha::OAI::Server::ListMetadataFormats'); + use_ok('Koha::OAI::Server::ListRecords'); + use_ok('Koha::OAI::Server::ListSets'); + use_ok('Koha::OAI::Server::Record'); + use_ok('Koha::OAI::Server::Repository'); + use_ok('Koha::OAI::Server::ResumptionToken'); +} + + +# Mocked CGI module in order to be able to send CGI parameters to OAI Server +my %param; +my $module = Test::MockModule->new('CGI'); +$module->mock('Vars', sub { %param; }); + +my $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; +$dbh->do('DELETE FROM issues'); +$dbh->do('DELETE FROM biblio'); +$dbh->do('DELETE FROM biblioitems'); +$dbh->do('DELETE FROM items'); + +# Add 10 biblio records +my @bibs = map { + my $record = MARC::Record->new(); + $record->append_fields( MARC::Field->new('245', '', '', 'a' => "Title $_" ) ); + my ($biblionumber) = AddBiblio($record, ''); + $biblionumber; +} (1..10); + +t::lib::Mocks::mock_preference('LibraryName', 'My Library'); +t::lib::Mocks::mock_preference('OAI::PMH', 1); +t::lib::Mocks::mock_preference('OAI-PMH:archiveID', 'TEST'); +t::lib::Mocks::mock_preference('OAI-PMH:ConfFile', '' ); +t::lib::Mocks::mock_preference('OAI-PMH:MaxCount', 3); +t::lib::Mocks::mock_preference('OAI-PMH:DeletedRecord', 'persistent'); + +%param = ( verb => 'ListMetadataFormats' ); +my ($response) = capture { Koha::OAI::Server::Repository->new(); }; +$response = XMLin($response); +my $now = DateTime->now . 'Z'; +my $expected = { + request => 'http://localhost', + responseDate => $now, + xmlns => 'http://www.openarchives.org/OAI/2.0/', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd', + ListMetadataFormats => { + metadataFormat => [ + { + metadataNamespace => 'http://www.openarchives.org/OAI/2.0/oai_dc/', + metadataPrefix=> 'oai_dc', + schema => 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd', + }, + { + metadataNamespace => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim', + metadataPrefix => 'marcxml', + schema => 'http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd', + }, + ], + }, +}; +is_deeply($response, $expected, "ListMetadataFormats"); + +%param = ( verb => 'ListIdentifiers' ); +($response) = capture { Koha::OAI::Server::Repository->new(); }; +$response = XMLin($response); +$now = DateTime->now . 'Z'; +$expected = { + request => 'http://localhost', + responseDate => $now, + xmlns => 'http://www.openarchives.org/OAI/2.0/', + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation' => 'http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd', + error => { + code => 'badArgument', + content => "Required argument 'metadataPrefix' was undefined", + }, +}; +is_deeply($response, $expected, "ListIdentifiers without metadaPrefix argument"); + +$dbh->rollback; --