View | Details | Raw Unified | Return to bug 14939
Collapse All | Expand All

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

Return to bug 14939