###################################################################### # # EPrints OAI 2.0 Handler # # Responds to incoming OAI requests # ###################################################################### # # This file is part of GNU EPrints 2. # # Copyright (c) 2000-2004 University of Southampton, UK. SO17 1BJ. # # EPrints 2 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. # # EPrints 2 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 EPrints 2; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # ###################################################################### #cjg need to recomment all subs use EPrints; # use FileHandle; # use IO::File; # use POSIX; # use XML::Writer; use URI::Escape; # use Time::Local; use strict; # New session my $session = new EPrints::Session(); exit( 0 ) unless( defined $session ); # Allowed exports (OAI restricts the prefixes that can be used) our %FORMATS = %{$session->get_repository->get_conf( "oai", "v2", "output_plugins" )}; # What are we begin asked? my $verb = $session->param( "verb" ); #$session->get_database->set_debug(1); my $NS = "http://www.openarchives.org/OAI/2.0/"; my $response = $session->make_element( "OAI-PMH", "xmlns"=>$NS, "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation"=>$NS." http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd" ); $response->appendChild( $session->render_data_element( 2, "responseDate", EPrints::Time::get_iso_timestamp() ) ); my @bits = (); my $request_desc = $session->make_element( "request" ); foreach my $attr ( "verb", "identifier", "metadataPrefix", "from", "until", "set", "resumptionToken" ) { my $value = $session->param( $attr ); # check it matches schema if( $attr eq "verb" ) { next unless( $value eq "Identify" || $value eq "GetRecord" || $value eq "ListRecords" || $value eq "ListIdentifiers" || $value eq "ListMetadataFormats" || $value eq "ListSets" ); } if( $attr eq "identifier" ) { next unless( $value =~ m/^[a-z]+:.*$/ ); } if( $attr eq "metadataPrefix" ) { next unless( $value =~ m/^[A-Za-z0-9attr!'$\(\)\+\-\.\*]+$/ ); } if( $attr eq "from" || $attr eq "until") { next unless( $value =~ m/^\d\d\d\d-\d\d-\d\dT(\d\d:\d\d:\d\dZ)?$/ ); } if( $attr eq "set" ) { next unless( $value =~ m/^([A-Za-z0-9_!'$\(\)\+\-\.\*])+(:[A-Za-z0-9_!'$\(\)\+\-\.\*]+)*$/ ); } # if( $attr eq "resumptionToken" ) { } # just any string # may be setting it to undef - but that won't matter as that # unsets it. next unless defined $value; $request_desc->setAttribute( $attr=>$value ); } my $url = $session->get_repository->get_conf( "base_url" ); $url .= $session->get_uri; $request_desc->appendChild( $session->make_text( $url ) ); $response->appendChild( $session->make_indent( 2 ) ); $response->appendChild( $request_desc ); $response->appendChild( render_verb( $session, $session->param( "verb" ) ) ); my $content = "text/xml"; if( $session->param( "debug" ) eq "yes" ) { $content = "text/plain"; } $session->send_http_header( content_type=>$content ); print <<END; <?xml version="1.0" encoding="UTF-8" ?> <?xml-stylesheet type='text/xsl' href='/oai2.xsl' ?> END print EPrints::XML::to_string( $response ); EPrints::XML::dispose( $response ); $session->terminate(); exit; # OAI 2 Error conditions: # # badArgument # badResumptionToken # badVerb # caonnot DisseminateFormat # idDoesNotExist # noRecordsMatch # noSetHierachy sub render_verb { my( $session , $verb ) = @_; if( !defined $verb ) { return render_oai_error( $session, "badVerb", "No verb was specified" ); } if( $verb eq "Identify" ) { return Identify( $session ); } if( $verb eq "GetRecord" ) { return GetRecord( $session ); } if( $verb eq "ListRecords" ) { return ListRecords( $session ); } if( $verb eq "ListIdentifiers" ) { return ListIdentifiers( $session ); } if( $verb eq "ListMetadataFormats" ) { return ListMetadataFormats( $session ); } if( $verb eq "ListSets" ) { return ListSets( $session ); } return render_oai_error( $session, "badVerb", "Unknown verb: '$verb'" ); } ###################################################################### # # Identify( $session ) # # Identify ourselves # ###################################################################### sub Identify { my( $session ) = @_; my( $args, $errors ) = get_oai_args( $session, [], [] ); return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); my $response = $session->make_element( "Identify" ); $response->appendChild( $session->render_data_element( 4, "repositoryName", $session->phrase( "archive_name" ) ) ); $response->appendChild( $session->render_data_element( 4, "baseURL", $session->get_repository->get_conf( "oai","v2","base_url" ) ) ); $response->appendChild( $session->render_data_element( 4, "protocolVersion", "2.0" ) ); $response->appendChild( $session->render_data_element( 4, "adminEmail", $session->get_repository->get_conf( "adminemail" ) ) ); # Later this may be either calcualted from the # database, or configurable. $response->appendChild( $session->render_data_element( 4, "earliestDatestamp", "0001-01-01" ) ); $response->appendChild( $session->render_data_element( 4, "deletedRecord", "persistent" ) ); $response->appendChild( $session->render_data_element( 4, "granularity", "YYYY-MM-DD" ) ); my $d1 = $session->make_element( "description" ); my $NS = "http://www.openarchives.org/OAI/2.0/oai-identifier"; my $XSD = "http://www.openarchives.org/OAI/2.0/oai-identifier.xsd"; my $oaiid = $session->make_element( "oai-identifier", "xmlns"=>$NS, "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation"=>"$NS $XSD" ); $d1->appendChild( $session->make_indent( 6 ) ); $d1->appendChild( $oaiid ); $response->appendChild( $session->make_indent( 4 ) ); $response->appendChild( $d1 ); $oaiid->appendChild( $session->render_data_element( 8, "scheme", "oai" ) ); $oaiid->appendChild( $session->render_data_element( 8, "repositoryIdentifier", $session->get_repository->get_conf( "oai","v2","archive_id" ) ) ); $oaiid->appendChild( $session->render_data_element( 8, "delimiter", ":" ) ); $oaiid->appendChild( $session->render_data_element( 8, "sampleIdentifier", $session->get_repository->get_conf( "oai","v2","sample_identifier" ) ) ); my $d2 = $session->make_element( "description" ); my $eprints = $session->make_element( "eprints", "xmlns"=>"http://www.openarchives.org/OAI/1.1/eprints", "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance", "xsi:schemaLocation"=>"http://www.openarchives.org/OAI/1.1/eprints http://www.openarchives.org/OAI/1.1/eprints.xsd" ); $d2->appendChild( $session->make_indent( 6 ) ); $d2->appendChild( $eprints ); $response->appendChild( $session->make_indent( 4 ) ); $response->appendChild( $d2 ); $eprints->appendChild( render_text_url( $session, "content", $session->get_repository->get_conf( "oai","content" ) ) ); $eprints->appendChild( render_text_url( $session, "metadataPolicy", $session->get_repository->get_conf( "oai","metadata_policy" ) ) ); $eprints->appendChild( render_text_url( $session, "dataPolicy", $session->get_repository->get_conf( "oai","data_policy" ) ) ); $eprints->appendChild( render_text_url( $session, "submissionPolicy", $session->get_repository->get_conf( "oai","submission_policy" ) ) ); foreach( @{$session->get_repository->get_conf( "oai","comments" )} ) { $eprints->appendChild( $session->render_data_element( 8, "comment", $_ ) ); } my $f = $session->make_doc_fragment(); $f->appendChild( $session->make_indent( 2 ) ); $f->appendChild( $response ); return $f; } ###################################################################### # # write_text_url( $writer, $name, $texturl ) # hashref # Write a TextURL type block to writer, of name $name. Block will # contain a text and/or url element, defined in %texturl. # If texturl contains neither then this method returns without action. # ###################################################################### sub render_text_url { my( $session, $name, $texturl ) = @_; my $f = $session->make_doc_fragment(); $f->appendChild( $session->make_indent( 8 ) ); my $e = $session->make_element( $name ); $f->appendChild( $e ); if ( defined $texturl->{"text"} ) { $e->appendChild( $session->render_data_element( 10, "text", $texturl->{"text"} ) ); } if ( defined $texturl->{"url"} ) { $e->appendChild( $session->render_data_element( 10, "URL", $texturl->{"url"} ) ); } return $f; } ###################################################################### # # GetRecord( $session ) # # Respond to a GetRecord verb: Retrieve a single metadata record # ###################################################################### sub GetRecord { my( $session ) = @_; my( $args, $errors ) = get_oai_args( $session, [ "identifier", "metadataPrefix" ], [] ); return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); if( !defined $session->get_repository->get_conf( "oai", "v2", "metadata_namespaces" )->{$args->{metadataPrefix}} ) { return render_oai_error( $session, "cannotDisseminateFormat", "Record not available as metadata type: ".$args->{metadataPrefix} ); } my $id = EPrints::OpenArchives::from_oai_identifier( $session, $args->{identifier} ); my $eprint = new EPrints::DataObj::EPrint( $session, $id, $session->get_repository->get_dataset( "archive" ) ); if( !defined $eprint ) { # try deleted records $eprint = new EPrints::DataObj::EPrint( $session, $id, $session->get_repository->get_dataset( "deletion" ) ); } my $response = $session->make_element( "GetRecord" ); # The eprint exists, so write the record # if the metadataFormat isn't available for # this record, only the header will be output. my $plugin_id = "Export::" . $FORMATS{ $args->{metadataPrefix} }; my $plugin = $session->plugin( $plugin_id ); unless( defined $plugin ) { EPrints::abort( "Could not find plugin $plugin_id" ); } $response->appendChild( $session->make_indent( 2 ) ); $response->appendChild( EPrints::OpenArchives::make_record( $session, $eprint, $plugin, 1 ) ); my $f = $session->make_doc_fragment(); $f->appendChild( $session->make_indent( 2 ) ); $f->appendChild( $response ); return $f; } ###################################################################### # # ListIdentifiers( $session ) # ListRecords( $session ) # # Respond to ListIdentifiers & ListRecords verbs. # ###################################################################### sub ListIdentifiers { my( $session ) = @_; return _list( $session, 1 ); } sub ListRecords { my( $session ) = @_; return _list( $session, 2 ); } sub _list { my( $session , $mode ) = @_; #mode 1 = ID #mode 2 = full metadata my $PAGESIZE = 100; # different params depending if we have a resumptionToken # or not my( $searchexp, $offset, $metadata_format ); if( defined $session->param( "resumptionToken" ) ) { my( $args, $errors ) = get_oai_args( $session, [ "resumptionToken" ], [] ); return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); unless( $args->{"resumptionToken"} =~ m#^(archive|deletion)/(\d+)/(\d+)(/(.*))?$# ) { return render_oai_error( $session, "badResumptionToken", "Token is invalid (does not match regexp)" ); } my $filters => $session->get_repository->get_conf( "oai", "filters" ); $filters = [] unless defined $filters; my $cache_id; ( $offset, $cache_id, $metadata_format ) = ( $2, $3, $5 ); $searchexp = EPrints::Search->new( dataset => $session->get_repository->get_dataset( "$1" ), session => $session, keep_cache => 1, filters => [ @{$filters}, ], cache_id => $cache_id ); unless( defined $searchexp ) { return render_oai_error( $session, "badResumptionToken", "Token has expired" ); } } else { my $optf = [ "until", "from", "set"]; my $reqf = [ "metadataPrefix" ]; # We ignore metadataPrefix if doing ListIdentifiers # which is not quite the Right Thing, but saves much CPU. my( $args, $errors ) = get_oai_args( $session, $reqf, $optf ); return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); $metadata_format = $args->{metadataPrefix}; if( !defined($FORMATS{ $metadata_format }) ) { return render_oai_error( $session, "cannotDisseminateFormat", "Record not available as metadata type: ".$args->{metadataPrefix} ); } my $date_range; my( $date, $date, $errors, $g1, $g2, $e ); $errors = []; if( defined $args->{from} ) { ( $date , $g1 , $e ) = munge_date( $session, $args->{from}, 0 ); push @{$errors}, @{$e}; $date_range = $date."-"; } return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); if( defined $args->{until} ) { ( $date , $g2 , $e ) = munge_date( $session, $args->{until}, 1 ); push @{$errors}, @{$e}; $date_range.= "-" if( !defined $date_range ); $date_range.= $date; } return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); if( defined $g1 && defined $g2 && $g1 ne $g2 ) { return render_oai_error( $session, "badArgument", "from and until dates have different granularity ($g1 and $g2)" ); } my $ds = $session->get_repository->get_dataset( "archive" ); my $filters => $session->get_repository->get_conf( "oai", "filters" ); $filters = [] unless defined $filters; $searchexp = new EPrints::Search( session => $session, keep_cache => 1, allow_blank => 1, filters => [ @{$filters}, ], dataset => $ds ); if( defined $args->{set} ) { my( $head , @tail ) = EPrints::OpenArchives::decode_setspec( $args->{set} ); my( $key , $value ) = split( /=/ , $head ); $value = pop @tail if( scalar @tail > 0 ); my $views = $session->get_repository->get_conf( "oai","sets" ); #cjg my $info; foreach( @{$views} ) { $info = $_ if( $_->{id} eq $key ); } if( !defined $info ) { return render_oai_error( $session, "badArgument", "Invalid set parameter; unknown key ( $key )" ); } my @fields; my $match = "EX"; foreach( split( "/", $info->{fields} ) ) { my $field = EPrints::Utils::field_from_config_string( $ds, $_ ); unless( $field->is_browsable() ) { # Eeep. This is really bad. Just die now. my $type = $field->get_type(); EPrints::abort( <<END ); Cannot generate OAI set for field "$_" - Type "$type" cannot be browsed. END } push @fields, $field; if( $field->is_type( "subject" ) ) { $match = "EQ"; } } $searchexp->add_field( \@fields, $value, $match ); } if( defined $date_range ) { $searchexp->add_field( $ds->get_field( "datestamp" ), $date_range ); } $offset = 0; if( !defined $searchexp ) { # something went wrong return render_oai_error( $session, "badArgument", "Could not make Search (system error)." ); } } $searchexp->perform_search(); my $count = $searchexp->count(); if( $count == 0 ) { ## End of archive items, time to do deleted ones. my $delsearchexp = $searchexp->clone; $delsearchexp->set_dataset( $session->get_repository->get_dataset( "deletion" ) ); $delsearchexp->perform_search(); $searchexp->dispose(); $searchexp = $delsearchexp; $count = $searchexp->count(); } if( $count == 0 ) { # no items at all return render_oai_error( $session, "noRecordsMatch", "No items match. None. None at all. Not even deleted ones." ); } my $cache = $searchexp->get_cache_id(); my $searchdsid = $searchexp->get_dataset()->id(); my %opts = (); $opts{status}="deleted" if( $searchdsid eq "deletion" ); my $response; my $mdtoken = ""; my $response; if( $mode == 1 ) { $response = $session->make_element( "ListIdentifiers" ); my @records = $searchexp->get_records( $offset, $PAGESIZE ); my $eprint; foreach $eprint ( @records ) { $response->appendChild( $session->make_indent( 2 ) ); $response->appendChild( EPrints::OpenArchives::make_header( $session, $eprint, 1 ) ); } } if( $mode == 2 ) { $response = $session->make_element( "ListRecords" ); my $plugin_id = "Export::" . $FORMATS{ $metadata_format }; my $plugin = $session->plugin( $plugin_id ); unless( defined $plugin ) { EPrints::abort( "Could not find plugin $plugin_id" ); } my @records = $searchexp->get_records( $offset, $PAGESIZE ); my $eprint; foreach $eprint ( @records ) { $response->appendChild( $session->make_indent( 2 ) ); $response->appendChild( EPrints::OpenArchives::make_record( $session, $eprint, $plugin, 1 ) ); } $mdtoken = "/".$metadata_format; } $searchexp->dispose(); my $tokenvalue = ""; if( $count > $offset+$PAGESIZE ) { $tokenvalue = $searchdsid."/". ($offset+$PAGESIZE)."/". $cache.$mdtoken; } elsif( $searchdsid eq "archive" ) { ## End of archive items, time to do deleted ones. my $delsearchexp = $searchexp->clone; $delsearchexp->set_dataset( $session->get_repository->get_dataset( "deletion" ) ); $delsearchexp->perform_search(); my $cache = $delsearchexp->get_cache_id(); my $count = $delsearchexp->count(); $delsearchexp->dispose(); # no point giving a token if we have no # deleted items. if( $count > 0 ) { $tokenvalue = "deletion/0/$cache".$mdtoken; } } if( EPrints::Utils::is_set( $tokenvalue ) ) { $response->appendChild( $session->render_data_element( 2, "resumptionToken", $tokenvalue ) ); } my $f = $session->make_doc_fragment(); $f->appendChild( $session->make_indent( 2 ) ); $f->appendChild( $response ); return $f; } ###################################################################### # # ListMetadataFormats( $session ) # ###################################################################### sub ListMetadataFormats { my( $session ) = @_; my( $args, $errors ) = get_oai_args( $session, [], [ "identifier" ] ); return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); my $eprint; if( defined $args->{identifier} ) { $eprint = new EPrints::DataObj::EPrint( $session, EPrints::OpenArchives::from_oai_identifier( $session, $args->{identifier} ), $session->get_repository->get_dataset( "archive" ) ); } my $response = $session->make_element( "ListMetadataFormats" ); foreach ( keys %FORMATS ) { my $plugin_id = "Export::" . $FORMATS{ $_ }; my $plugin = $session->plugin( $plugin_id ) or next; if( defined($eprint) ) { my $md = $eprint->export( $FORMATS{ $_ } ) or next; EPrints::XML::dispose( $md ); } my $mdf = $session->make_element( "metadataFormat" ); $mdf->appendChild( $session->render_data_element( 4, "metadataPrefix", $_ ) ); $mdf->appendChild( $session->render_data_element( 4, "schema", $plugin->{ 'schemaLocation' } )); $mdf->appendChild( $session->render_data_element( 4, "metadataNamespace", $plugin->{ 'xmlns' } )); $response->appendChild( $session->make_indent( 2 ) ); $response->appendChild( $mdf ); } my $f = $session->make_doc_fragment(); $f->appendChild( $session->make_indent( 2 ) ); $f->appendChild( $response ); return $f; } ###################################################################### # # ListSets( $session ) # # Respond to a ListSets verb. # ###################################################################### sub ListSets { my( $session ) = @_; my( $args, $errors ) = get_oai_args( $session, [], [ 'resumptionToken' ] ); return join_errors( $session, $errors ) if( scalar @{$errors} > 0 ); if( defined $args->{resumptionToken} ) { return render_oai_error( $session, "badResumptionToken", "Resumption Tokens not supported for ListSets" ); } my @sets = (); my %setnames = (); my $response = $session->make_element( "ListSets" ); my $ds = $session->get_repository->get_dataset( "archive" ); my $ds_del = $session->get_repository->get_dataset( "deletion" ); my $viewconf = $session->get_repository->get_conf( "oai","sets" ); my $info; foreach $info ( @{$viewconf} ) { my $fieldname; my %v = (); foreach $fieldname ( split( "/" , $info->{fields} ) ) { my $field = EPrints::Utils::field_from_config_string( $ds, $fieldname ); if( $field->is_type( "subject" ) ) { my $topsubj = EPrints::DataObj::Subject->new( $session, $field->get_property( "top" ) ); my $i; foreach $i ( @{$topsubj->get_subjects( 0, 0, 1 )} ) { my @kb = split( ":", $i->[0] ); foreach( @kb ) { $_ = EPrints::OpenArchives::encode_setspec( $_ ); } my $key = join( ":", @kb ); $v{$key} = $i->[1]; } } else { my $v1 = $field->get_values( $session, $ds ); my $delfield = $field->clone(); #cjg why clone with new style datasets? #$delfield->set_dataset( $ds_del ); my $v2 = $delfield->get_values( $session, $ds_del ); foreach( @{$v1}, @{$v2} ) { my $key = EPrints::OpenArchives::encode_setspec( $_ ); if( !defined $key ) { $key=""; } $v{$key} = EPrints::Utils::tree_to_utf8( $field->get_value_label( $session, $_ ) ); } } } unless( $info->{allow_null} ) { delete $v{""}; } foreach( keys %v ) { my $set = $session->make_element( "set" ); $response->appendChild( $session->make_indent( 2 ) ); $response->appendChild( $set ); my $spec = EPrints::OpenArchives::encode_setspec( $info->{id}."=" ).$_; $set->appendChild( $session->render_data_element( 4, "setSpec", $spec ) ); my $name = $session->get_view_name( $ds, $info->{id} )." = ".$v{$_}; $set->appendChild( $session->render_data_element( 4, "setName", $name ) ); } } my $f = $session->make_doc_fragment(); $f->appendChild( $session->make_indent( 2 ) ); $f->appendChild( $response ); return $f; } ###################################################################### # # send_http_error( $session, $code, $message ) # # Send an HTTP error as a response # ###################################################################### sub send_http_error { my( $session, $code, $message ) = @_; my $r = Apache->request; $r->content_type( 'text/html' ); $r->status_line( "$code $message" ); $r->send_http_header; my $title = "Error $code in OAI request"; $r->print( <<END ); <html> <head><title>$title</title></head> <body> <h1>$title</h1> <p>$message</p> </body> END } sub render_oai_error { my( $session, $code, $message ) = @_; return $session->render_data_element( 2, "error", $message, code => $code ); } sub get_oai_args { my( $session, $required, $optional ) = @_; my %a; foreach( @{$required}, @{$optional} ) { $a{$_}=1; } $a{verb} = 1; my %args; my @errors; foreach( $session->param() ) { if( $a{$_} == 1 ) { my @p = $session->param( $_ ); $args{$_} = $p[0]; delete $a{$_}; if( scalar @p > 1 ) { # Repeated Arg push @errors, render_oai_error( $session, "badArgument", "Repeated argument: $_" ); } next; } push @errors, render_oai_error( $session, "badArgument", "Illegal argument: $_" ); } foreach( @{$required} ) { next unless( $a{ $_ } ); push @errors, render_oai_error( $session, "badArgument", "Missing required argument: $_" ); } if( defined $args{identifier} && $args{identifier} !~ m/^oai:[a-zA-Z][a-zA-Z0-9\-]*(\.[a-zA-Z][a-zA-Z0-9\-]+)+:[a-zA-Z0-9\-_\.!~\*'\(\);\/\?:\@\&=\+\$,\%]+$/ ) { push @errors, render_oai_error( $session, "badArgument", "identifier does not match regexp: $args{identifier}" ); } return( \%args, \@errors ); } sub join_errors { my( $session, $errors ) = @_; my $f = $session->make_doc_fragment; foreach( @{$errors} ) { $f->appendChild( $_ ); } return $f; } sub munge_date { my( $session, $string, $roundup ) = @_; if( $string !~ m/^(\d\d\d\d)-(\d\d)-(\d\d)(T(\d\d):(\d\d):(\d\d(\.\d+)?)Z)?$/ ) { return( "", "", [render_oai_error( $session, "badArgument", "not valid datetime: $string" )] ); } my( $year, $month, $day, $hour, $min, $sec ) = ( $1 , $2 , $3 , $5, $6 , $7 ); if( defined $hour ) { return( "", "", [render_oai_error( $session, "badArgument", "Max granularity is YYYY-MM-DD: $string" )] ); } my $granularity = "YYYY-MM-DD"; # $granularity = "YYYY-MM" unless( defined $day ); # $granularity = "YYYY" unless( defined $month ); # YYYY and YYYY-MM granularities disappeard between OAI2.0alpha3 # and OAI2.0 but I'll keep the code around just in case. # if( !defined $month ) # { # $month = ( $roundup ? 12 : 1 ); # } # if( !defined $day ) # { # $day = 1; # if( $roundup ) # { # $month+=1; # if( $month == 13 ) { $month = 1; $year+=1; } # my $gt = timegm(0,0,0,$day,$month-1,$year); # # $gt is one month too far so we want one day less. # my @bits = gmtime( $gt - 24*60*60 ); # $day = $bits[3]; # $month = $bits[4]+1; # $year = $bits[5]+1900; # } # } my $date = sprintf( "%04d-%02d-%02d",$year,$month,$day ); return( $date, $granularity, []); }