Bug 24531: Fix OAI-PMH sets for records with repeated fields
[koha.git] / C4 / OAI / Sets.pm
blobad33aa3cdf393ce5c4fc50690828db109fab728d
1 package C4::OAI::Sets;
3 # Copyright 2011 BibLibre
5 # This file is part of Koha.
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.
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.
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>.
20 =head1 NAME
22 C4::OAI::Sets - OAI Sets management functions
24 =head1 DESCRIPTION
26 C4::OAI::Sets contains functions for managing storage and editing of OAI Sets.
28 OAI Set description can be found L<here|http://www.openarchives.org/OAI/openarchivesprotocol.html#Set>
30 =cut
32 use Modern::Perl;
33 use C4::Context;
35 use vars qw(@ISA @EXPORT);
37 BEGIN {
38 require Exporter;
39 @ISA = qw(Exporter);
40 @EXPORT = qw(
41 &GetOAISets &GetOAISet &GetOAISetBySpec &ModOAISet &DelOAISet &AddOAISet
42 &GetOAISetsMappings &GetOAISetMappings &ModOAISetMappings
43 &GetOAISetsBiblio &ModOAISetsBiblios &AddOAISetsBiblios
44 &CalcOAISetsBiblio &UpdateOAISetsBiblio &DelOAISetsBiblio
48 =head1 FUNCTIONS
50 =head2 GetOAISets
52 $oai_sets = GetOAISets;
54 GetOAISets return a array reference of hash references describing the sets.
55 The hash references looks like this:
58 'name' => 'set name',
59 'spec' => 'set spec',
60 'descriptions' => [
61 'description 1',
62 'description 2',
63 ...
67 =cut
69 sub GetOAISets {
70 my $dbh = C4::Context->dbh;
71 my $query = qq{
72 SELECT * FROM oai_sets
74 my $sth = $dbh->prepare($query);
75 $sth->execute;
76 my $results = $sth->fetchall_arrayref({});
78 $query = qq{
79 SELECT description
80 FROM oai_sets_descriptions
81 WHERE set_id = ?
83 $sth = $dbh->prepare($query);
84 foreach my $set (@$results) {
85 $sth->execute($set->{'id'});
86 my $desc = $sth->fetchall_arrayref({});
87 foreach (@$desc) {
88 push @{$set->{'descriptions'}}, $_->{'description'};
92 return $results;
95 =head2 GetOAISet
97 $set = GetOAISet($set_id);
99 GetOAISet returns a hash reference describing the set with the given set_id.
101 See GetOAISets to see what the hash looks like.
103 =cut
105 sub GetOAISet {
106 my ($set_id) = @_;
108 return unless $set_id;
110 my $dbh = C4::Context->dbh;
111 my $query = qq{
112 SELECT *
113 FROM oai_sets
114 WHERE id = ?
116 my $sth = $dbh->prepare($query);
117 $sth->execute($set_id);
118 my $set = $sth->fetchrow_hashref;
120 $query = qq{
121 SELECT description
122 FROM oai_sets_descriptions
123 WHERE set_id = ?
125 $sth = $dbh->prepare($query);
126 $sth->execute($set->{'id'});
127 my $desc = $sth->fetchall_arrayref({});
128 foreach (@$desc) {
129 push @{$set->{'descriptions'}}, $_->{'description'};
132 return $set;
135 =head2 GetOAISetBySpec
137 my $set = GetOAISetBySpec($setSpec);
139 Returns a hash describing the set whose spec is $setSpec
141 =cut
143 sub GetOAISetBySpec {
144 my $setSpec = shift;
146 return unless defined $setSpec;
148 my $dbh = C4::Context->dbh;
149 my $query = qq{
150 SELECT *
151 FROM oai_sets
152 WHERE spec = ?
153 LIMIT 1
155 my $sth = $dbh->prepare($query);
156 $sth->execute($setSpec);
158 return $sth->fetchrow_hashref;
161 =head2 ModOAISet
163 my $set = {
164 'id' => $set_id, # mandatory
165 'spec' => $spec, # mandatory
166 'name' => $name, # mandatory
167 'descriptions => \@descriptions, # optional, [] to remove descriptions
169 ModOAISet($set);
171 ModOAISet modify a set in the database.
173 =cut
175 sub ModOAISet {
176 my ($set) = @_;
178 return unless($set && $set->{'spec'} && $set->{'name'});
180 if(!defined $set->{'id'}) {
181 warn "Set ID not defined, can't modify the set";
182 return;
185 my $dbh = C4::Context->dbh;
186 my $query = qq{
187 UPDATE oai_sets
188 SET spec = ?,
189 name = ?
190 WHERE id = ?
192 my $sth = $dbh->prepare($query);
193 $sth->execute($set->{'spec'}, $set->{'name'}, $set->{'id'});
195 if($set->{'descriptions'}) {
196 $query = qq{
197 DELETE FROM oai_sets_descriptions
198 WHERE set_id = ?
200 $sth = $dbh->prepare($query);
201 $sth->execute($set->{'id'});
203 if(scalar @{$set->{'descriptions'}} > 0) {
204 $query = qq{
205 INSERT INTO oai_sets_descriptions (set_id, description)
206 VALUES (?,?)
208 $sth = $dbh->prepare($query);
209 foreach (@{ $set->{'descriptions'} }) {
210 $sth->execute($set->{'id'}, $_) if $_;
216 =head2 DelOAISet
218 DelOAISet($set_id);
220 DelOAISet remove the set with the given set_id
222 =cut
224 sub DelOAISet {
225 my ($set_id) = @_;
227 return unless $set_id;
229 my $dbh = C4::Context->dbh;
230 my $query = qq{
231 DELETE oai_sets, oai_sets_descriptions, oai_sets_mappings
232 FROM oai_sets
233 LEFT JOIN oai_sets_descriptions ON oai_sets_descriptions.set_id = oai_sets.id
234 LEFT JOIN oai_sets_mappings ON oai_sets_mappings.set_id = oai_sets.id
235 WHERE oai_sets.id = ?
237 my $sth = $dbh->prepare($query);
238 $sth->execute($set_id);
241 =head2 AddOAISet
243 my $set = {
244 'id' => $set_id, # mandatory
245 'spec' => $spec, # mandatory
246 'name' => $name, # mandatory
247 'descriptions => \@descriptions, # optional
249 my $set_id = AddOAISet($set);
251 AddOAISet adds a new set and returns its id, or undef if something went wrong.
253 =cut
255 sub AddOAISet {
256 my ($set) = @_;
258 return unless($set && $set->{'spec'} && $set->{'name'});
260 my $set_id;
261 my $dbh = C4::Context->dbh;
262 my $query = qq{
263 INSERT INTO oai_sets (spec, name)
264 VALUES (?,?)
266 my $sth = $dbh->prepare($query);
267 if( $sth->execute($set->{'spec'}, $set->{'name'}) ) {
268 $set_id = $dbh->last_insert_id(undef, undef, 'oai_sets', undef);
269 if($set->{'descriptions'}) {
270 $query = qq{
271 INSERT INTO oai_sets_descriptions (set_id, description)
272 VALUES (?,?)
274 $sth = $dbh->prepare($query);
275 foreach( @{ $set->{'descriptions'} } ) {
276 $sth->execute($set_id, $_) if $_;
279 } else {
280 warn "AddOAISet failed";
283 return $set_id;
286 =head2 GetOAISetsMappings
288 my $mappings = GetOAISetsMappings;
290 GetOAISetsMappings returns mappings for all OAI Sets.
292 Mappings define how biblios are categorized in sets.
293 A mapping is defined by six properties:
296 marcfield => 'XXX', # the MARC field to check
297 marcsubfield => 'Y', # the MARC subfield to check
298 operator => 'A', # the operator 'equal' or 'notequal'; 'equal' if ''
299 marcvalue => 'zzzz', # the value to check
300 rule_operator => 'and|or|undef', # the operator between the rules
301 rule_order => 'n' # the order of the rule for the mapping
304 If defined in a set mapping, a biblio which have at least one 'Y' subfield of
305 one 'XXX' field equal to 'zzzz' will belong to this set.
307 GetOAISetsMappings returns a hashref of arrayrefs of hashrefs.
308 The first hashref keys are the sets IDs, so it looks like this:
310 $mappings = {
311 '1' => [
313 marcfield => 'XXX',
314 marcsubfield => 'Y',
315 operator => 'A',
316 marcvalue => 'zzzz',
317 rule_operator => 'and|or|undef',
318 rule_order => 'n'
325 '2' => [...],
329 =cut
331 sub GetOAISetsMappings {
332 my $dbh = C4::Context->dbh;
333 my $query = qq{
334 SELECT * FROM oai_sets_mappings ORDER BY set_id, rule_order
336 my $sth = $dbh->prepare($query);
337 $sth->execute;
339 my $mappings = {};
340 while(my $result = $sth->fetchrow_hashref) {
341 push @{ $mappings->{$result->{'set_id'}} }, {
342 marcfield => $result->{'marcfield'},
343 marcsubfield => $result->{'marcsubfield'},
344 operator => $result->{'operator'},
345 marcvalue => $result->{'marcvalue'},
346 rule_operator => $result->{'rule_operator'},
347 rule_order => $result->{'rule_order'}
351 return $mappings;
354 =head2 GetOAISetMappings
356 my $set_mappings = GetOAISetMappings($set_id);
358 Return mappings for the set with given set_id. It's an arrayref of hashrefs
360 =cut
362 sub GetOAISetMappings {
363 my ($set_id) = @_;
365 return unless $set_id;
367 my $dbh = C4::Context->dbh;
368 my $query = qq{
369 SELECT *
370 FROM oai_sets_mappings
371 WHERE set_id = ?
372 ORDER BY rule_order
374 my $sth = $dbh->prepare($query);
375 $sth->execute($set_id);
377 my @mappings;
378 while(my $result = $sth->fetchrow_hashref) {
379 push @mappings, {
380 marcfield => $result->{'marcfield'},
381 marcsubfield => $result->{'marcsubfield'},
382 operator => $result->{'operator'},
383 marcvalue => $result->{'marcvalue'},
384 rule_operator => $result->{'rule_operator'},
385 rule_order => $result->{'rule_order'}
389 return \@mappings;
392 =head2 ModOAISetMappings {
394 my $mappings = [
396 marcfield => 'XXX',
397 marcsubfield => 'Y',
398 operator => 'A',
399 marcvalue => 'zzzz'
403 ModOAISetMappings($set_id, $mappings);
405 ModOAISetMappings modifies mappings of a given set.
407 =cut
409 sub ModOAISetMappings {
410 my ($set_id, $mappings) = @_;
412 return unless $set_id;
414 my $dbh = C4::Context->dbh;
415 my $query = qq{
416 DELETE FROM oai_sets_mappings
417 WHERE set_id = ?
419 my $sth = $dbh->prepare($query);
420 $sth->execute($set_id);
421 if(scalar @$mappings > 0) {
422 $query = qq{
423 INSERT INTO oai_sets_mappings (set_id, marcfield, marcsubfield, operator, marcvalue, rule_operator, rule_order)
424 VALUES (?,?,?,?,?,?,?)
426 $sth = $dbh->prepare($query);
427 foreach (@$mappings) {
428 $sth->execute($set_id, $_->{'marcfield'}, $_->{'marcsubfield'}, $_->{'operator'}, $_->{'marcvalue'}, $_->{'rule_operator'}, $_->{'rule_order'});
433 =head2 GetOAISetsBiblio
435 $oai_sets = GetOAISetsBiblio($biblionumber);
437 Return the OAI sets where biblio appears.
439 Return value is an arrayref of hashref where each element of the array is a set.
440 Keys of hash are id, spec and name
442 =cut
444 sub GetOAISetsBiblio {
445 my ($biblionumber) = @_;
447 my $dbh = C4::Context->dbh;
448 my $query = qq{
449 SELECT oai_sets.*
450 FROM oai_sets
451 LEFT JOIN oai_sets_biblios ON oai_sets_biblios.set_id = oai_sets.id
452 WHERE biblionumber = ?
454 my $sth = $dbh->prepare($query);
456 $sth->execute($biblionumber);
457 return $sth->fetchall_arrayref({});
460 =head2 DelOAISetsBiblio
462 DelOAISetsBiblio($biblionumber);
464 Remove a biblio from all sets
466 =cut
468 sub DelOAISetsBiblio {
469 my ($biblionumber) = @_;
471 return unless $biblionumber;
473 my $dbh = C4::Context->dbh;
474 my $query = qq{
475 DELETE FROM oai_sets_biblios
476 WHERE biblionumber = ?
478 my $sth = $dbh->prepare($query);
479 return $sth->execute($biblionumber);
482 =head2 CalcOAISetsBiblio
484 my @sets = CalcOAISetsBiblio($record, $oai_sets_mappings);
486 Return a list of set ids the record belongs to. $record must be a MARC::Record
487 and $oai_sets_mappings (optional) must be a hashref returned by
488 GetOAISetsMappings
490 =cut
492 sub CalcOAISetsBiblio {
493 my ($record, $oai_sets_mappings) = @_;
495 return unless $record;
497 $oai_sets_mappings ||= GetOAISetsMappings;
499 my @biblio_sets;
500 foreach my $set_id (keys %$oai_sets_mappings) {
502 my $rules = [];
503 foreach my $mapping (@{ $oai_sets_mappings->{$set_id} }) {
504 next if not $mapping;
505 my $rule_operator = $mapping->{'rule_operator'};
506 my $result = _evalRule($record, $mapping);
508 # First rule or 'or' rule is always pushed
509 if (!@$rules || $rule_operator eq 'or') {
510 push @$rules, [$result];
511 next;
514 # 'and' rule is pushed in the last 'or' rule
515 push @{$rules->[-1]}, $result;
518 my @evaluated_and;
519 foreach my $ruleset (@$rules) {
520 if (0 < grep /0/, @{$ruleset}) {
521 push @evaluated_and, 0;
522 } else {
523 push @evaluated_and, 1;
527 if (grep /1/, @evaluated_and) {
528 push @biblio_sets, $set_id;
532 return @biblio_sets;
535 # Does the record match a given mapping rule?
536 sub _evalRule {
537 my $record = shift;
538 my $mapping = shift;
540 my $field = $mapping->{'marcfield'};
541 my $subfield = $mapping->{'marcsubfield'};
542 my $operator = $mapping->{'operator'};
543 my $value = $mapping->{'marcvalue'};
545 my @all_subfield_values;
546 # Get all the fields with the given tag
547 my @fields = $record->field($field);
548 # Iterate over all the fields
549 foreach my $field ( @fields ) {
550 # Get the values from all the subfields with the given subfield code
551 if ( my @subfield_values = $field->subfield($subfield) ) {
552 push @all_subfield_values, @subfield_values;
556 if ($operator eq 'notequal') {
557 if(0 == grep /^$value$/, @all_subfield_values) {
558 return 1;
561 else {
562 if(0 < grep /^$value$/, @all_subfield_values) {
563 return 1;
566 return 0;
570 =head2 ModOAISetsBiblios
572 my $oai_sets_biblios = {
573 '1' => [1, 3, 4], # key is the set_id, and value is an array ref of biblionumbers
574 '2' => [],
577 ModOAISetsBiblios($oai_sets_biblios);
579 ModOAISetsBiblios deletes all records from oai_sets_biblios table and calls AddOAISetsBiblios.
580 This table is then used in opac/oai.pl.
582 =cut
584 sub ModOAISetsBiblios {
585 my $oai_sets_biblios = shift;
587 return unless ref($oai_sets_biblios) eq "HASH";
589 my $dbh = C4::Context->dbh;
590 my $query = qq{
591 DELETE FROM oai_sets_biblios
593 my $sth = $dbh->prepare($query);
594 $sth->execute;
595 AddOAISetsBiblios($oai_sets_biblios);
598 =head2 UpdateOAISetsBiblio
600 UpdateOAISetsBiblio($biblionumber, $record);
602 Update OAI sets for one biblio. The two parameters are mandatory.
603 $record is a MARC::Record.
605 =cut
607 sub UpdateOAISetsBiblio {
608 my ($biblionumber, $record) = @_;
610 return unless($biblionumber and $record);
612 my $sets_biblios;
613 my @sets = CalcOAISetsBiblio($record);
614 foreach (@sets) {
615 push @{ $sets_biblios->{$_} }, $biblionumber;
617 DelOAISetsBiblio($biblionumber);
618 AddOAISetsBiblios($sets_biblios);
621 =head2 AddOAISetsBiblios
623 my $oai_sets_biblios = {
624 '1' => [1, 3, 4], # key is the set_id, and value is an array ref of biblionumbers
625 '2' => [],
628 ModOAISetsBiblios($oai_sets_biblios);
630 AddOAISetsBiblios insert given infos in oai_sets_biblios table.
631 This table is then used in opac/oai.pl.
633 =cut
635 sub AddOAISetsBiblios {
636 my $oai_sets_biblios = shift;
638 return unless ref($oai_sets_biblios) eq "HASH";
640 my $dbh = C4::Context->dbh;
641 my $query = qq{
642 INSERT INTO oai_sets_biblios (set_id, biblionumber)
643 VALUES (?,?)
645 my $sth = $dbh->prepare($query);
646 foreach my $set_id (keys %$oai_sets_biblios) {
647 foreach my $biblionumber (@{$oai_sets_biblios->{$set_id}}) {
648 $sth->execute($set_id, $biblionumber);