Newer
Older
Digital_Repository / OARiNZ / DIY / deb_package / eprints-3.0 / cgi / oai2
nstanger on 7 Jun 2007 25 KB - Added debian package source.
######################################################################
#
#  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, []);
}