Bug 10853: All existing routing to get a CSV should return a MARC csv
[koha.git] / opac / oai.pl
index 73f352f..7aa5a7d 100755 (executable)
 #!/usr/bin/perl
 
+# Copyright Biblibre 2008
+#
+# 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 2 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+
 use strict;
+use warnings;
 
+use CGI qw/:standard -oldstyle_urls/;
+use vars qw( $GZIP );
 use C4::Context;
-use C4::Biblio;
 
-=head1 OAI-PMH for koha
 
-This file is an implementation of the OAI-PMH protocol for koha. Its purpose
-is to share metadata in Dublin core format with harvester like PKP-Harverster.
-Presently, all the bibliographic records managed by the runing koha instance
-are publicly shared (as the opac is).
+BEGIN {
+    eval { require PerlIO::gzip };
+    $GZIP = ($@) ? 0 : 1;
+}
 
-=head1 Package MARC::Record::KOHADC
+unless ( C4::Context->preference('OAI-PMH') ) {
+    print
+        header(
+            -type       => 'text/plain; charset=utf-8',
+            -charset    => 'utf-8',
+            -status     => '404 OAI-PMH service is disabled',
+        ),
+        "OAI-PMH service is disabled";
+    exit;
+}
 
-This package is a sub-class of the MARC::File::USMARC. It add methods and functions
-to map the content of a marc record (of any flavor) to Dublin core.
-As soon as it is possible, mapping between marc fields and there semantic
-are got from ::GetMarcFromKohaField fonction from C4::Biblio (see also the "Koha
-to MARC mapping" preferences).
+my @encodings = http('HTTP_ACCEPT_ENCODING');
+if ( $GZIP && grep { defined($_) && $_ eq 'gzip' } @encodings ) {
+    print header(
+        -type               => 'text/xml; charset=utf-8',
+        -charset            => 'utf-8',
+        -Content-Encoding   => 'gzip',
+    );
+    binmode( STDOUT, ":gzip" );
+}
+else {
+    print header(
+        -type       => 'text/xml; charset=utf-8',
+        -charset    => 'utf-8',
+    );
+}
 
-=cut
+binmode STDOUT, ':encoding(UTF-8)';
+my $repository = C4::OAI::Repository->new();
 
-package MARC::Record::KOHADC;
-use vars ('@ISA');
-@ISA = qw(MARC::Record);
+# __END__ Main Prog
 
-use MARC::File::USMARC;
 
-sub new { # Get a MAR::Record as parameter and bless it as MARC::Record::KOHADC
-       shift;
-       bless shift;
-}
+#
+# Extends HTTP::OAI::ResumptionToken
+# A token is identified by:
+# - metadataPrefix
+# - from
+# - until
+# - offset
+# 
+package C4::OAI::ResumptionToken;
 
-sub subfield {
-    my $self = shift;
-    my ($t,$sf) = @_;
+use strict;
+use warnings;
+use HTTP::OAI;
 
-    return $self->SUPER::subfield( @_ ) unless wantarray;
+use base ("HTTP::OAI::ResumptionToken");
 
-    my @field = $self->field($t);
-    my @list = ();
-    my $f;
 
-    foreach $f ( @field ) {
-               push( @list, $f->subfield( $sf ) );
-    }
-    return @list;
-}
+sub new {
+    my ($class, %args) = @_;
 
-sub getfields {
-my $marc = shift;
-my @result = ();
+    my $self = $class->SUPER::new(%args);
 
-        foreach my $kohafield ( @_ ) {
-                my ( $field, $subfield ) = ::GetMarcFromKohaField( $kohafield, '' );
-                push( @result, $field < 10 ? $marc->field( $field )->as_string() : $marc->subfield( $field, $subfield ) );
+    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 );
         }
-#        @result>1 ? \@result : $result[0];
-       \@result;
-}  
+        $offset = $args{ offset } || 0;
+        $set = $args{set};
+    }
 
-sub Status {
-  my $self = shift;
-       undef;
-}
+    $self->{ metadata_prefix } = $metadata_prefix;
+    $self->{ offset          } = $offset;
+    $self->{ from            } = $from;
+    $self->{ until           } = $until;
+    $self->{ set             } = $set;
 
-sub Title {
-  my $self = shift;
-       $self->getfields('biblio.title');
-}
+    $self->resumptionToken(
+        join( ':', $metadata_prefix, $offset, $from, $until, $set ) );
+    $self->cursor( $offset );
 
-sub Creator {
-  my $self = shift;
-       $self->getfields('biblio.author');
+    return $self;
 }
 
-sub Subject {
-  my $self = shift;
-       $self->getfields('bibliosubject.subject');
-}
+# __END__ C4::OAI::ResumptionToken
 
-sub DateStamp {
-  my $self = shift;
-       my ($d,$h) = split( ' ', $self->{'biblio.timestamp'} );
-       $d . "T" . $h . "Z";
-}
 
-sub Date {
-  my $self = shift;
-    my ($str) = @{$self->getfields('biblioitems.publicationyear')};
-    my ($y,$m,$d) = (substr($str,0,4), substr($str,4,2), substr($str,6,2));
 
-    $y=1970 unless($y>0); $m=1 unless($m>0); $d=1 unless($d>0);
+package C4::OAI::Identify;
 
-    sprintf( "%.4d-%.2d-%.2d", $y,$m,$d);
-}
+use strict;
+use warnings;
+use HTTP::OAI;
+use C4::Context;
 
-sub Description {
-  my $self = shift;
-       undef;
-}
+use base ("HTTP::OAI::Identify");
 
-sub Identifier {
-  my $self = shift;
-  my $id = $self->getfields('biblio.biblionumber')->[0];
-
-# get url of this script and assume that OAI server is in the same place as opac-detail script
-# and build a direct link to the record.
-  my $uri = $ENV{'SCRIPT_URI'};
-  $uri= "http://" . $ENV{'HTTP_HOST'} . $ENV{'REQUEST_URI'} unless( $uri ); # SCRIPT_URI doesn't exist on all httpd server
-  $uri =~ s#[^/]+$##;  
-       [
-               C4::Context->preference("OAI-PMH:archiveID") .":" .$id, 
-               "${uri}opac-detail.pl?bib=$id",
-               @{$self->getfields('biblioitems.isbn', 'biblioitems.issn')}
-       ];
-}
+sub new {
+    my ($class, $repository) = @_;
 
-sub Language {
-  my $self = shift;
-       undef;
-}
+    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       => 'no',
+    );
 
-sub Type {
-  my $self = shift;
-       $self->getfields('biblioitems.itemtype');
-}
+    # 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" );
 
-sub Publisher {
-  my $self = shift;
-       $self->getfields('biblioitems.publishercode');
-}
+    $self->compression( 'gzip' );
 
-sub Set {
-my $set = &OAI::KOHA::Set();
-       [ map( $_=$_->[0], @$set) ];
+    return $self;
 }
 
-=head1 The OAI::KOHA package
+# __END__ C4::OAI::Identify
 
-This package is a subclass of the OAI::DC data provider. It overides needed methods
-and provide the links between the OAI-PMH request and the koha application.
-The data used in answers are from the koha table I<bibio>.
 
-=cut
 
-package OAI::KOHA;
+package C4::OAI::ListMetadataFormats;
 
-use C4::OAI::DC;
-use vars ('@ISA');
-@ISA = ("C4::OAI::DC");
+use strict;
+use warnings;
+use HTTP::OAI;
 
-=head2 Set
+use base ("HTTP::OAI::ListMetadataFormats");
 
-return the Set list to the I<verb=ListSets> query. Data are from the 'OAI-PMH:Set' preference.
+sub new {
+    my ($class, $repository) = @_;
 
-=cut
+    my $self = $class->SUPER::new();
 
-sub Set {
-#   [
-#      ['BRISE','Experimental unimarc set for BRISE network'],
-#      ['BRISE:EMSE','EMSE set in BRISE network']
-#   ];
-#
-# A blinder correctement
-       [ map( $_ = [ split(",", $_)], split( "\n",C4::Context->preference("OAI-PMH:Set") ) ) ];
+    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;
 }
 
-=head2 new
+# __END__ C4::OAI::ListMetadataFormats
 
-The new method is the constructor for this class. It doesn't have any parameters and 
-get required data from koha preferences. Koha I<LibraryName> is used to identify the
-OAI-PMH repository, I<OAI-PMH:MaxCount> is used to set the maximun number of records
-returned at the same time in answers to I<verb=ListRecords> or I<verb=ListIdentifiers>
-queries.
 
-The method return a blessed reference.
 
-=cut
+package C4::OAI::Record;
 
-# constructor
-sub new
-{
-   my $classname = shift;
-   my $self = $classname->SUPER::new ();
+use strict;
+use warnings;
+use HTTP::OAI;
+use HTTP::OAI::Metadata::OAI_DC;
 
-   # set configuration
-   $self->{'repositoryName'} = C4::Context->preference("LibraryName");
-   $self->{'MaxCount'} = C4::Context->preference("OAI-PMH:MaxCount");
-   $self->{'adminEmail'} = C4::Context->preference("KohaAdminEmailAddress");
+use base ("HTTP::OAI::Record");
 
-   bless $self, $classname;
-   return $self;
-}
+sub new {
+    my ($class, $repository, $marcxml, $timestamp, $setSpecs, %args) = @_;
 
-=head2 dispose
+    my $self = $class->SUPER::new(%args);
 
-The dispose method is used as a destructor. It call just the SUPER::dispose method.
+    $timestamp =~ s/ /T/, $timestamp .= 'Z';
+    $self->header( new HTTP::OAI::Header(
+        identifier  => $args{identifier},
+        datestamp   => $timestamp,
+    ) );
 
-=cut
+    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 ) );
 
-# destructor
-sub dispose
-{
-   my ($self) = @_;
-   $self->SUPER::dispose ();
+    return $self;
 }
 
-# now date
-sub now {
-my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime( time );
+# __END__ C4::OAI::Record
+
 
-        sprintf( "%.4d-%.2d-%.2d", $year+1900, $mon+1,$mday );
+
+package C4::OAI::GetRecord;
+
+use strict;
+use warnings;
+use HTTP::OAI;
+use C4::OAI::Sets;
+
+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 marcxml, timestamp
+        FROM   biblioitems
+        WHERE  biblionumber=? " );
+    my $prefix = $repository->{koha_identifier} . ':';
+    my ($biblionumber) = $args{identifier} =~ /^$prefix(.*)/;
+    $sth->execute( $biblionumber );
+    my ($marcxml, $timestamp);
+    unless ( ($marcxml, $timestamp) = $sth->fetchrow ) {
+        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",
+                ) ] ,
+        );
+    }
+
+    my $oai_sets = GetOAISetsBiblio($biblionumber);
+    my @setSpecs;
+    foreach (@$oai_sets) {
+        push @setSpecs, $_->{spec};
+    }
+
+    #$self->header( HTTP::OAI::Header->new( identifier  => $args{identifier} ) );
+    $self->record( C4::OAI::Record->new(
+        $repository, $marcxml, $timestamp, \@setSpecs, %args ) );
+
+    return $self;
 }
 
-# build the resumptionTocken fom ($metadataPrefix,$offset,$from,$until)
+# __END__ C4::OAI::GetRecord
 
-=head2 buildResumptionToken and parseResumptionToken
 
-Theses two functions are used to manage resumption tokens. The choosed syntax is simple as
-possible, a token is only the metadata prefix, the offset in the full answer, the from and 
-the until date (in the yyyy-mm-dd format) joined by ':' caracter.
 
-I<buildResumptionToken> get the four elements as parameters and return the ':' separated 
-string.
+package C4::OAI::ListIdentifiers;
 
-I<parseResumptionToken> is used to set the default values to the from and until date, the 
-metadata prefix using the resumption tocken if necessary. This function have four parameters
-(from,until,metadata prefix and resumption tocken) which can be undefined and return every
-time this list of values correctly set. The missing values are set with defaults: offset=0,
-from= 1970-01-01 and until is set to current date.
+use strict;
+use warnings;
+use HTTP::OAI;
+use C4::OAI::Sets;
 
-=cut
+use base ("HTTP::OAI::ListIdentifiers");
+
+
+sub new {
+    my ($class, $repository, %args) = @_;
 
-sub buildResumptionToken {
-        join( ':', @_ );
+    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 $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 DATE(timestamp) >= ? AND DATE(timestamp) <= ? ";
+    $sql .= " AND oai_sets_biblios.set_id = ? " if defined $set;
+    $sql .= "
+        LIMIT $repository->{'koha_max_count'}
+        OFFSET $token->{'offset'}
+    ";
+    my $sth = $dbh->prepare( $sql );
+    my @bind_params = ($token->{'from'}, $token->{'until'});
+    push @bind_params, $set->{'id'} if defined $set;
+    $sth->execute( @bind_params );
+
+    my $pos = $token->{offset};
+    while ( my ($biblionumber, $timestamp) = $sth->fetchrow ) {
+        $timestamp =~ s/ /T/, $timestamp .= 'Z';
+        $self->identifier( new HTTP::OAI::Header(
+            identifier => $repository->{ koha_identifier} . ':' . $biblionumber,
+            datestamp  => $timestamp,
+        ) );
+        $pos++;
+    }
+    $self->resumptionToken(
+        new C4::OAI::ResumptionToken(
+            metadataPrefix  => $token->{metadata_prefix},
+            from            => $token->{from},
+            until           => $token->{until},
+            offset          => $pos,
+            set             => $token->{set}
+        )
+    ) if ($pos > $token->{offset});
+
+    return $self;
 }
 
-# parse the resumptionTocken
-sub parseResumptionToken {
-my ($from, $until, $metadataPrefix, $resumptionToken) = @_;
-my $offset = 0;
+# __END__ C4::OAI::ListIdentifiers
 
-        if( $resumptionToken ) {
-                ($metadataPrefix,$offset,$from,$until) = split( ':', $resumptionToken );
-        }
+package C4::OAI::Description;
+
+use strict;
+use warnings;
+use HTTP::OAI;
+use HTTP::OAI::SAXHandler qw/ :SAX /;
+
+sub new {
+    my ( $class, %args ) = @_;
+
+    my $self = {};
 
-        $from  = "1970-01-01" unless( $from );
-        $until = &now unless( $until );
-        ($metadataPrefix, $offset, $from, $until );
+    if(my $setDescription = $args{setDescription}) {
+        $self->{setDescription} = $setDescription;
+    }
+    if(my $handler = $args{handler}) {
+        $self->{handler} = $handler;
+    }
+
+    bless $self, $class;
+    return $self;
 }
 
-=head2 Archive_ListSets
+sub set_handler {
+    my ( $self, $handler ) = @_;
 
-return the full list Set to the I<verb=ListSets> query. Data are from the 'OAI-PMH:Set' preference.
+    $self->{handler} = $handler if $handler;
 
-=cut
+    return $self;
+}
 
-# get full list of sets from the archive
-sub Archive_ListSets
-{
-       &Set();
+sub generate {
+    my ( $self ) = @_;
+
+    g_data_element($self->{handler}, 'http://www.openarchives.org/OAI/2.0/', 'setDescription', {}, $self->{setDescription});
+
+    return $self;
 }
-                              
-=head2 Archive_GetRecord
 
-This method select the record specified as its first parameter from the koha I<biblio>
-table and return a reference to a MARC::Record::KOHADC object. 
+# __END__ C4::OAI::Description
 
-=cut
+package C4::OAI::ListSets;
 
-# get a single record from the archive
-sub Archive_GetRecord
-{
-   my ($self, $identifier, $metadataFormat) = @_;
-   my $dbh = C4::Context->dbh;
-   my $sth = $dbh->prepare("SELECT biblionumber,timestamp FROM biblio WHERE biblionumber=?");
-   my $prefixID = C4::Context->preference("OAI-PMH:archiveID"); $prefixID=qr{$prefixID:};
+use strict;
+use warnings;
+use HTTP::OAI;
+use C4::OAI::Sets;
 
-   $identifier =~ s/^$prefixID//;
+use base ("HTTP::OAI::ListSets");
 
-   $sth->execute( $identifier );
+sub new {
+    my ( $class, $repository, %args ) = @_;
 
-   if( my $r = $sth->fetchrow_hashref() ) {
-       my $marc = new MARC::Record::KOHADC( ::GetMarcBiblio( $identifier ) );
-       $marc->{'biblio.timestamp'} = $r->{'timestamp'};
-       return $marc ;
-   }
+    my $self = HTTP::OAI::ListSets->new(%args);
 
-   $self->AddError ('idDoesNotExist', 'The value of the identifier argument is unknown or illegal in this repository');
-   undef;
+    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;
 }
 
-=head2 Archive_ListRecords
+# __END__ C4::OAI::ListSets;
 
-This method return a list of 'MaxCount' references to MARC::Record::KOHADC object build from the 
-koha I<biblio> table according to its parameters : set, from and until date, metadata prefix 
-and resumption token.
+package C4::OAI::ListRecords;
 
-=cut
+use strict;
+use warnings;
+use HTTP::OAI;
+use C4::OAI::Sets;
+
+use base ("HTTP::OAI::ListRecords");
 
-# list metadata records from the archive
-sub Archive_ListRecords
-{
-   my ($self, $set, $from, $until, $metadataPrefix, $resumptionToken) = @_;
-
-   my @allrows = ();
-   my $marc;
-   my $offset;
-   my $tokenInfo;
-   my $dbh = C4::Context->dbh;
-   my $sth = $dbh->prepare("SELECT biblionumber,timestamp FROM biblio WHERE DATE(timestamp) >= ? and DATE(timestamp) <= ? LIMIT ? OFFSET ?");
-   my $count;
-
-        ($metadataPrefix, $offset, $from, $until ) = &parseResumptionToken($from, $until, $metadataPrefix, $resumptionToken);
-
-warn( "Archive_ListRecords : $set, $from, $until, $metadataPrefix, $resumptionToken\n");
-       $sth->execute( $from,$until,$self->{'MaxCount'}?$self->{'MaxCount'}:100000, $offset );
-
-       while( my $r = $sth->fetchrow_hashref() ) { 
-               my $marc = new MARC::Record::KOHADC( ::GetMarcBiblio( $r->{'biblionumber'} ) );
-               $marc->{'biblio.timestamp'} = $r->{'timestamp'};
-               push( @allrows, $marc );
-       } 
-
-       $sth = $dbh->prepare("SELECT count(*) FROM biblioitems WHERE DATE(timestamp) >= ? and DATE(timestamp) <= ?"); 
-       $sth->execute($from, $until);
-       ( $count ) = $sth->fetchrow_array();
-
-       unless( @allrows ) {
-               $self->AddError ('noRecordsMatch', 'The combination of the values of arguments results in an empty set');
-       }
-
-       if( $offset + $self->{'MaxCount'} < $count ) { # Not at the end
-               $offset = $offset + $self->{'MaxCount'};
-               $resumptionToken = &buildResumptionToken($metadataPrefix,$offset,$from,$until);
-               $tokenInfo = { 'completeListSize' => $count, 'cursor' => $offset };
-       }
-       else {
-               $resumptionToken = '';
-               $tokenInfo = {};
-       }
-       ( \@allrows, $resumptionToken, $metadataPrefix, $tokenInfo );
+
+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 $sql = "
+        SELECT biblioitems.biblionumber, biblioitems.marcxml, biblioitems.timestamp
+        FROM biblioitems
+    ";
+    $sql .= " JOIN oai_sets_biblios ON biblioitems.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 .= "
+        LIMIT $repository->{'koha_max_count'}
+        OFFSET $token->{'offset'}
+    ";
+
+    my $sth = $dbh->prepare( $sql );
+    my @bind_params = ($token->{'from'}, $token->{'until'});
+    push @bind_params, $set->{'id'} if defined $set;
+    $sth->execute( @bind_params );
+
+    my $pos = $token->{offset};
+    while ( my ($biblionumber, $marcxml, $timestamp) = $sth->fetchrow ) {
+        my $oai_sets = GetOAISetsBiblio($biblionumber);
+        my @setSpecs;
+        foreach (@$oai_sets) {
+            push @setSpecs, $_->{spec};
+        }
+        $self->record( C4::OAI::Record->new(
+            $repository, $marcxml, $timestamp, \@setSpecs,
+            identifier      => $repository->{ koha_identifier } . ':' . $biblionumber,
+            metadataPrefix  => $token->{metadata_prefix}
+        ) );
+        $pos++;
+    }
+    $self->resumptionToken(
+        new C4::OAI::ResumptionToken(
+            metadataPrefix  => $token->{metadata_prefix},
+            from            => $token->{from},
+            until           => $token->{until},
+            offset          => $pos,
+            set             => $token->{set}
+        )
+    ) if ($pos > $token->{offset});
+
+    return $self;
 }
 
-package main;
+# __END__ C4::OAI::ListRecords
 
-=head1 Main package
 
-The I<main> function is the starting point of the service. The first step is
-to verify if the service is enable using the 'OAI-PMH' preference value
-(See Koha systeme preferences).
 
-If the service is enable, it create a new instance of the OAI::KOHA data
-provider (see before) and run the service.
+package C4::OAI::Repository;
 
-=cut
+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);
 
-sub disable {
-       print "Status:404 OAI-PMH service is disabled\n";
-       print "Content-type: text/plain\n\n";
+    $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
 
-       print "OAI-PMH service is disable.\n";
+    # 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 main
-{
-   return &disable() unless( C4::Context->preference('OAI-PMH') );
 
-   my $OAI = new OAI::KOHA();
-   $OAI->Run;
-   $OAI->dispose;
+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;
 }
 
-main;
 
-1;
+
+=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
+
+
+