#!/usr/bin/perl

#Copyright Massachusetts Institute of technology, 2000.
#Written by Eric Prud'hommeaux

# $Id: bookmark,v 1.21 2004/07/06 18:51:36 eric Exp $

#####
# What It Does:

#####
# set up module environment
use strict;

#BEGIN {unshift@INC,('../../..');}
use W3C::Rdf::CGIApp;
@BookmarkshunQueryObject::ISA = qw(AnnotationQueryObject);
@W3C::Annotations::cgibin::BookmarkScript::BookmarkHTMLPresenter::ISA = qw(W3C::Rdf::CGIApp::HTMLPresenter);

package W3C::Annotations::cgibin::BookmarkScript;
use vars qw(@ISA);
@ISA = qw(W3C::Annotations::AnnotationApp);

use W3C::Annotations::AnnotationApp qw($SCRIPT_HOME_URI_PROP 
				       $SCHEMA_ANNOTATION_ATTRIBUTION_PROP 
				       $SCHEMA_ANNOTATION_EMAIL_PROP 
				       $SCHEMA_ANNOTATION_GIVEN_PROP 
				       $SCHEMA_ANNOTATION_FAMILY_PROP 
				       $NS_ANNOTATION $NS_THREAD $NS_HTTP $NS_DC
				       $NS_PALM $NS_ATTRIBUTIONS);

use W3C::Util::W3CDebugCGI;
use W3C::Util::Exception;
use W3C::Util::Properties;
use W3C::Annotations::UserRecord qw($RW_read);
use W3C::Annotations::DBMUserRecords;
use W3C::Rdf::Atoms qw($RDF_SCHEMA_URI $RDFS_SCHEMA_URI);

use vars qw($REVISION $VERSION);
$REVISION = '$Id: bookmark,v 1.21 2004/07/06 18:51:36 eric Exp $ ';
$VERSION=0.95;

use vars qw($DEFAULT_dbClass $DEFAULT_dbParms);
$DEFAULT_dbClass = "W3C::Annotations::DBMUserRecords";
$DEFAULT_dbParms = "-type => 'ndbm', -file => '/etc/apache/conf/users'";

use vars qw(%ParmMapping %PathMapping);
%ParmMapping = (
		# active (POST) methods
		'post_bookmark' => 'RDF_INPUT', 
		'w3c_resource' => 'BASE_URI', 
		'w3c_rdfUri' => 'RDF_URI', 
		'w3c_append' => 'APPEND', 
		'delete_source' => 'DELETE_SOURCE', 
		'replace_source' => 'REPLACE_SOURCE', 

		# passive (GET) methods
		'w3c_recalls' => 'B_RECALLS', 
		'w3c_replyTree' => 'REPLY_TREE', 
		'bookmark' => 'QUERY_BOOKMARKSHUN', 
		'reply' => 'QUERY_REPLY', 
		'attribution' => 'QUERY_ATTRIBUTION', 
		'w3c_bookmark' => 'BOOKMARKSHUN_REQ', 
		'explain' => 'EXPLAIN',
		'statistics' => 'QUERY_STATISTICS', 
		'w3c_hasTopic' => 'HAS_TOPIC_REQ', 
		'w3c_subTopic' => 'SUB_TOPIC_REQ', 

		# algae query specifiers
		'w3c_algaeQuery' => 'ALGAE_QUERY', 
		'rdftype' => 'ALGAE_DELETE_TYPE', 

		# output/content control
		'w3c_forceText' => 'RDF_FORCETEXT', 
		'w3c_forceHTML' => 'RDF_FORCEHTML', 
		'w3c_ephemoralDB' => 'EPHEMORAL_DB', 

		'w3c_submit' => 'DOIT', 
		);

%PathMapping = (
		'attribution' => 'QUERY_ATTRIBUTION', 
		'body' => 'QUERY_BODY', 
		'bookmark' => 'QUERY_BOOKMARKSHUN', 
		'reply' => 'QUERY_REPLY', 
		'statistics' => 'QUERY_STATISTICS', 
		);

use vars qw($BOOKMARKS_RULES);
$BOOKMARKS_RULES = <<EORULE
(namespace '(bm http://www.w3.org/2002/01/bookmark#
             rdfs http://www.w3.org/2000/01/rdf-schema#) 
 fwrule (head '((bm::subTopicOf ?a ?b)(bm::subTopicOf ?b ?c)) 
	 body '((bm::subTopicOf ?a ?c))) 
 fwrule (head '((bm::hasTopic ?a ?b)(bm::subTopicOf ?b ?c)) 
	 body '((bm::hasTopic ?a ?c))))
EORULE
    ; #'

use vars qw($NS_BOOKMARKS);
$NS_BOOKMARKS = 'http://www.w3.org/2002/01/bookmark#';

$AnnotationQueryObject::InputBookmarkQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?bookmark ${NS_BOOKMARKS}Bookmark ?r1 ?s1 \$attribUri) 
       (${NS_BOOKMARKS}hasTopic ?bookmark ?hasTopic)
       (${NS_BOOKMARKS}recalls ?bookmark ?recalls)
      ) :collect \'(?bookmark))";

$AnnotationQueryObject::InputReplyQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?bookmark ${NS_THREAD}Reply ?r1 ?s1 \$attribUri) 
       (${NS_THREAD}inReplyTo ?bookmark ?bookmarks)
       (${NS_BOOKMARKS}hasTopic ?bookmark ?hasTopic)
      ) :collect \'(?bookmark ?bookmarks))";

$AnnotationQueryObject::BookmarkshunQuery = "(ask
     '((?p \$self->{-bookmarkshun} ?o) 
      ) :collect '(?p ?o))";

$AnnotationQueryObject::ReplyQuery = "(ask
     '((?p \$self->{-reply} ?o) 
      ) :collect '(?p ?o))";

$AnnotationQueryObject::FatBookmarkshunQuery = "(ask
     '((${RDF_SCHEMA_URI}type \$self->{-bookmarkshun} ${NS_BOOKMARKS}Bookmark) 
       (${NS_BOOKMARKS}recalls \$self->{-bookmarkshun} ?recalls)
       (${NS_BOOKMARKS}hasTopic \$self->{-bookmarkshun} ?hasTopic)
       (${NS_DC}creator \$self->{-bookmarkshun} ?creator)
       (${NS_ANNOTATION}E-mail ?creator ?email)
       (${NS_ANNOTATION}name ?creator ?name)
       (${NS_ANNOTATION}created \$self->{-bookmarkshun} ?created)
       (${NS_DC}date \$self->{-bookmarkshun} ?date)
      ) :collect '(?recalls ?hasTopic ?creator ?created ?date))";

$AnnotationQueryObject::BodyQuery = "(ask
     '((${NS_HTTP}Body \$self->{-bodyId} ?bodyData ?r ?b ?attrib) 
       (${NS_HTTP}ContentType \$self->{-bodyId} ?contentType) 
      ) :collect '(?bodyData ?contentType ?attrib))";

$AnnotationQueryObject::RecallsQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?bookmark ${NS_BOOKMARKS}Bookmark) 
       (${NS_BOOKMARKS}recalls ?bookmark \$self->{-recalls})
      ) :collect '(?bookmark))";

$AnnotationQueryObject::SubjectQuery = "(ask
     '((?p \$self->{-subject} ?o) 
      ) :collect '(?p ?o))";

$AnnotationQueryObject::RootQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?reply ${NS_THREAD}Reply) 
       (${NS_THREAD}root ?reply \$self->{-subject})
       (${NS_DC}title ?reply ?title)
      ) :collect '(?reply ?title))";

$AnnotationQueryObject::InReplyToQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?reply ${NS_THREAD}Reply) 
       (${NS_THREAD}inReplyTo ?reply \$self->{-subject})
      ) :collect '(?reply))";


$AnnotationQueryObject::FatBookmarksQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?bookmark ${NS_BOOKMARKS}Bookmark) 
       (${NS_BOOKMARKS}recalls ?bookmark \$self->{-recalls})
       (${NS_BOOKMARKS}hasTopic ?bookmark ?hasTopic)
       (${NS_DC}creator ?bookmark ?creator)
       (${NS_ANNOTATION}E-mail ?creator ?email)
       (${NS_ANNOTATION}name ?creator ?name)
       (${NS_ANNOTATION}created ?bookmark ?created)
       (${NS_DC}date ?bookmark ?date)
      ) :collect '(?bookmark ?hasTopic ?creator ?created ?date))";

$AnnotationQueryObject::DocumentQuery = "(ask
     '((?p ?s ?o ?r ?b \$self->{-attribution})
      ) :collect '(?p ?s ?o))";

$AnnotationQueryObject::AttributionQuery = "(ask
     '((${NS_BOOKMARKS}recalls \$self->{-bookmarkshun} ?href ?r ?b ?attrib)
      ) :collect '(?attrib))";

$AnnotationQueryObject::HasTopicQuery = "(ask
     '((${RDF_SCHEMA_URI}type ?bookmark ${NS_BOOKMARKS}Bookmark) 
       (${NS_BOOKMARKS}recalls ?bookmark ?recalls)
       (${NS_BOOKMARKS}hasTopic ?bookmark \$self->{-hasTopic})
      ) :collect '(?bookmark ?recalls))";

$AnnotationQueryObject::SubTopicQuery = "(ask
     '((${NS_BOOKMARKS}subTopicOf ?topic \$self->{-hasTopic})
      ) :collect '(?topic))";

$AnnotationQueryObject::SuperTopicQuery = "(ask
     '((${NS_BOOKMARKS}subTopicOf \$self->{-hasTopic} ?topic)
      ) :collect '(?topic))";

#$AnnotationQueryObject::AttributionQuery = "(ask
#     '((${RDF_SCHEMA_URI}type \$self->{-bookmarkshun} ${NS_BOOKMARKS}Bookmark ?r ?b ?attrib) 
#       (${NS_BOOKMARKS}recalls \$self->{-bookmarkshun} ?recalls)
#      ) :collect '(?attrib))";

$bookmark::SUBMIT_APPEND_STR = 'append';

$bookmark::RDF_HEAD = <<EOHEAD;
<r:RDF xmlns:r="$RDF_SCHEMA_URI"
       xmlns:b="$NS_BOOKMARKS"
       xmlns:a="$NS_ANNOTATION"
       xmlns:http="$NS_HTTP"
       xmlns:dc="$NS_DC"
       xmlns:t="$NS_THREAD"
       xmlns="http://www.w3.org/1999/xhtml"
       xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
EOHEAD
    ;

$bookmark::RDF_FOOT = <<EOFOOT;
</r:RDF>
EOFOOT
    ;

#####
# main - either bookmark or show source

my ($query, $bookmark);

eval {
    $W3C::Util::W3CDebugCGI::DEBUG_SESSION = $ARGV[1]; # use a session id like 957296047.909868 or -2;
    $query = new W3C::Util::W3CDebugCGI($0, $ARGV[0] eq 'DEBUG', 
					{-dieNoOpen => 1,
					 -logExt => '.log', 
					 -storeIn => '/tmp', 
					 -rerun => 'w3c_rerun', 
					 -mergeQueryAndPOST => 1});

    $bookmark = new W3C::Annotations::cgibin::BookmarkScript($query);
    my $presenter = $bookmark->execute();
    print $presenter->flush(200);
}; if ($@) {
    my $sessionId = $query ? $query->getSessionId : undef;
    if (my $ex = &catch('W3C::Http::HttpMessageException')) {
	if ($bookmark && $bookmark->{RDF_DB}) {
	    $bookmark->{RDF_DB}->disconnect;
	    delete $bookmark->{ACL_REPOSITORY};
	}
	my $message = $ex->getHttpMessage();
	$message->addHeader('Session-Id', $sessionId) if (defined $sessionId);
	print $message->toString;
    } elsif (my $ex = &catch('W3C::Rdf::CGIApp::AppException')) {
	print $ex->toString($sessionId);
    } elsif (my $ex = &catch('W3C::Util::Exception')) {
	print "Status: 500\nContent-type: text/html\n\n";
	print "<pre>".$ex->toString."</pre>\n";
	if ($sessionId) {
	    print "<p>Session-id: $sessionId</p>\n";
	}
    } else {
	print "Status: 500\n\n";
	print "died with $@";
	if ($sessionId) {
	    print "\n\nSession-id: $sessionId\n";
	}
    }
}

sub new {
    my ($proto, $read) = @_;
    my $class = ref $proto || $proto;
    my $self = $class->SUPER::new($read, {-parmMapping => \%ParmMapping, -pathMapping => \%PathMapping});

    # attach to user database
    my $userDatabase = $self->{PROPERTIES}->getI('auth.database.class') || $DEFAULT_dbClass;
    my $parameters = $self->{PROPERTIES}->getI('auth.database.parms') || $DEFAULT_dbParms;
    my $evalMe = "\$self->{USER_RECORDS} = new $userDatabase(-errorHandler => \$self, $parameters);";
    eval $evalMe;
    if ($@) {
	&throw(new W3C::Util::Exception(-message => "failed to execute $evalMe: $!"));
    }

    $self->{SHORT_TITLE} => 'bookmark';
    return $self;
}

sub execute {
    my ($self) = @_;
    my $bookmarkshunObject = new BookmarkshunQueryObject(-filter => sub {
	# Add the attributions to each graph we serialize.

	my ($tmpDB, $attrib, $bookmarkshun, $statements) = @_;
	push (@{$statements->[0]}, @{$self->addIdStatements($tmpDB, $attrib, $bookmarkshun)});
    });
    my @titles = ();

    # grab some stuff from the properties
    $self->{ADMIN_IDS} = {};
    my @ids = $self->{PROPERTIES}->getI('auth.admin.user');
    foreach my $id (@ids) {
	my $idUri = $self->mapWebIdToUri($id);
	$self->{ADMIN_IDS}{$idUri} = $idUri;
    }

    # make interface appropriate to the accept header
    $self->{PRESENTER} = $self->makePresenter({-htmlPresenter => 'W3C::Annotations::cgibin::BookmarkScript::BookmarkHTMLPresenter'}); # @presenterParms

    # get authentication parameters
    my $auth = $self->getAuthenticatedUser();
    if (my $realSession = $self->{READ}->getRealSession()) {
	my $user = $realSession->getEnv('REMOTE_USER');
	my $callersAuth = $self->mapWebIdToUri($user);
	if ($self->checkEquivOrAdminAuth($callersAuth, $auth)) {
	    # we are allowing a user or admin user to rerun a session
	} else {
	    &throw($self->failAuth($callersAuth, 'rerun', $realSession->param('w3c_rerun')));
	}
    }

    # Add special foward chaining bookmarks rules to the DB rule cache.
    my $queryHandler = $self->{RDF_DB}->getAlgaeInterface;
    my ($nodes, $selects, $messages, $proofs) = $queryHandler->algae($BOOKMARKS_RULES, undef, {-uniqueResults => 1});

    # Deletions and Replacements
    my $deleteAttribution = undef;
    if ($self->{DELETE_SOURCE}) {
	$self->{DELETE_SOURCE} = $self->urlize($self->{DELETE_SOURCE}, '/bookmark/');
	$deleteAttribution = $self->{DELETE_SOURCE};
	push (@titles, "delete $deleteAttribution");
    } elsif ($self->{REPLACE_SOURCE}) {
	$self->{REPLACE_SOURCE} = $self->urlize($self->{REPLACE_SOURCE}, '/bookmark/');
	$deleteAttribution = $self->{REPLACE_SOURCE};
	push (@titles, "replace $deleteAttribution");
    }
    if ($deleteAttribution) {
	$bookmarkshunObject->{-bookmarkshun} = $deleteAttribution;
	my ($results, $selects, $messages, $statements, $query) = 
	    $bookmarkshunObject->templateReq($self->{RDF_DB}, $AnnotationQueryObject::AttributionQuery);
	if (@$results == 0) {
	    my $struct = [[[[ "${NS_BOOKMARKS}recalls", $bookmarkshunObject->{-bookmarkshun}, '?recalls', '?attrib']], 
			   ['?recalls', '?attrib']]];
	    &throw($self->htmlReply(404, 
				    'text/html', "Not Found", "No bookmark \"$deleteAttribution\" found</pre>".
				    $self->algaeLink($struct)));
	}
	if (@$results != 1) {
	    my $struct = [[[[ "${NS_BOOKMARKS}recalls", $bookmarkshunObject->{-bookmarkshun}, '?recalls', '?attrib']], 
			   ['?recalls', '?attrib']]];
	    &throw($self->htmlReply(500, 
				    'text/html', "Server Error", "More than one bookmark \"$deleteAttribution\" found</pre>".
				    $self->algaeLink($struct)));
	}
	my $document = $results->[0][0];
	my $storedAuth = $statements->[0][0]->getAttribution->getAuth;

	# Only the creator or admin user may delete.
	my $method = $self->{REPLACE_SOURCE} ? 'PUT' : 'DELETE';
	if (!($self->checkAuth($auth, $method, $document) > 0 && 
	      $self->checkEquivOrAdminAuth($auth, $storedAuth))) {
	    &throw($self->failAuth($auth, $method, $document));
	}

	# Don't delete anything with replies.
	$bookmarkshunObject->{-subject} = $deleteAttribution;
	($results, $selects, $messages, $statements, $query) = 
	    $bookmarkshunObject->templateReq($self->{RDF_DB}, $AnnotationQueryObject::InReplyToQuery);
	if (@$results != 0) {
	    my $struct = [[[['?p', '?s', $self->{-thread}]], 
			   ['?p', '?s']]];
	    &throw($self->htmlReply(509, 
				    'text/html', "Protocol Vaguery", "Declined to delete \"$deleteAttribution\" as it has replies</pre>".
				    $self->algaeLink($struct)));
	}
	$self->{RDF_DB}->getAttribution($W3C::Rdf::Atoms::AttributionType_SOURCE, $document, $storedAuth, undef, undef, 0, 1);
	$self->{BASE_URI} = $document->getUri;
    }

    if ($self->{QUERY_BODY}) {
	$self->{QUERY_BODY} = $self->urlize($self->{QUERY_BODY}, '/body/');
	$bookmarkshunObject->{-bodyId} = $self->{QUERY_BODY};
	my $bodyInfo = $bookmarkshunObject->bodyInfo($self->{RDF_DB});
	if (ref $bodyInfo eq 'ARRAY') {
	    my $bodyAttribution = $bodyInfo->[2];
	    if (1 || 
		$self->checkAuth($auth, 'GET', $bodyAttribution->{-bodyId}) > 0 || 
		$auth == $bodyAttribution->getAuth) {

		# Make the body fragment a nice, stand-alone document.
		my $bodyText = "<?xml version=\"1.0\" ?>\n$bodyInfo->[1]";
		my $measuredSize = length $bodyText;
		my $message = $self->simpleReply(200, $bodyInfo->[0], $measuredSize, $bodyText);
		&throw(new W3C::Http::HttpMessageException(-httpMessage => $message));
		#$self->{RDF_FORCETYPE} = [$bodyInfo->[0], 'MessagePresenter'];
		#return $self->makePresenter(undef, -httpMessage => $message);
	    } else {
		&throw($self->failAuth($auth, 'GET', $bodyAttribution->{-bodyId}));
	    }
	} else {
	    my $struct = [[[['?p', $bookmarkshunObject->{-bodyId}, '?o']], ['?p', '?o']]];
	    &throw($self->htmlReply(404, 
				    'text/html', "Not Found", "Unable to locate \"$self->{QUERY_BODY}\" with <pre>$bodyInfo</pre>".
				    $self->algaeLink($struct)));
	}
	return;
    }

    if ($ENV{'REQUEST_METHOD'} eq "GET" &&
	(($ENV{'QUERY_STRING'} eq "" && $ENV{'PATH_INFO'} eq "") 
	 || $self->{EXPLAIN} eq "true")) {
	if (my $ov = $self->{PROPERTIES}->getI('Overview')) {
	    if ($ov =~ m/^http:\/\//) {
		&throw($self->htmlReply(307, 'text/html', "Temporary Redirect", 
					"<p>go see <a href=\"$ov\">$ov</a></p>", {Location => $ov}));
	    } else {
		local *OV;
		if (open (OV, "<$ov")) {
		    while (my $line = <OV>) {
			$self->{PRESENTER}->printOK($line);
		    }
		    close OV;
		    return $self->{PRESENTER};
		} else {
		    &throw($self->htmlReply(500, 'text/html', "Configuration Error", 
					    "<p>could not find overview page <em>$ov</em></p>"));
		}
	    }
	}
    }

    if ($self->{RDF_INPUT}) {
	if ($self->checkAuth($auth, 'POST', $self->getSelfUri) > 0) {
	    eval {
		$self->parse($self->{PRESENTER}, $auth);
		my $found = $bookmarkshunObject->inputReq($self->{RDF_DB}, $self->{PRESENTER}, 
							  $self->{RDF_PARSER}, $self, 
							  $deleteAttribution ? scalar $self->decomposeUid($deleteAttribution) : undef, 
							  [[$AnnotationQueryObject::InputBookmarkQuery, 
							    [['bookmark', '-bookmarkshun']]]]);
		if ($found = 0) {
		    push (@titles, "accept bookmark $bookmarkshunObject->{-bookmarkshun}");
		} else {
		    push (@titles, "accepted data of unknown type");
		}
	    }; if ($@) {if (my $ex = &catch('W3C::Util::NoSuchFileException')) {
		&throw($self->htmlReply(500, 'text/html', "Configuration Error", 
					"<p>configuration file \"".$ex->getFile."\" missing.</p>"));
		# return print $self->confFileMissing($ex->getFile);
	    } elsif (my $ex = &catch('W3C::Util::Exception')) {
		push (@{$self->{PRE_MESSAGES}}, split("\n", $ex->toString));
		$self->{OVERRIDE_DISPLAY_RDF} = $self->{RDF_INPUT};
		# return print $self->unknownException($ex);
	    } else {
		&throw($self->htmlReply(500, 'text/html', "Script Error", 
					    "<p>bookmark encountered error: <em>$@</em>.</p>"));
		# return print $self->unknownException(new W3C::Util::Exception(-message => "perl error: $@"));
	    }}
	} else {
	    &throw($self->failAuth($auth, 'POST', $self->getSelfUri));
	}
    }

    if ($self->{BOOKMARKSHUN_REQ} || $self->{QUERY_BOOKMARKSHUN}) {
	if ($self->{QUERY_BOOKMARKSHUN}) {
	    $self->{QUERY_BOOKMARKSHUN} = $self->urlize($self->{QUERY_BOOKMARKSHUN}, '/bookmark/');
	    $bookmarkshunObject->{-bookmarkshun} = $self->{QUERY_BOOKMARKSHUN};
	} else {
	    $self->{BOOKMARKSHUN_REQ} = $self->urlize($self->{BOOKMARKSHUN_REQ}, undef);
	    $bookmarkshunObject->{-bookmarkshun} = $self->{BOOKMARKSHUN_REQ};
	}
	push (@titles, "bookmarkshun $bookmarkshunObject->{-bookmarkshun}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::BookmarkshunQuery, $bookmarkshunObject->{-bookmarkshun}, 
				      'bookmark details: ', 'found no bookmark matching ', 
				      [[[['?p', $self->{-bookmarkshun}, '?o']], ['?p', '?o']]], 
				      $auth);
    }

    if ($self->{HAS_TOPIC_REQ}) {
	$bookmarkshunObject->{-hasTopic} = $self->{HAS_TOPIC_REQ};
	push (@titles, "bookmarks with topic $bookmarkshunObject->{-hasTopic}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::HasTopicQuery, $bookmarkshunObject->{-hasTopic}, 
				      'topic details: ', 'found no bookmark with topic ', 
				      [[[['?p', '?s', $self->{-hasTopic}]], ['?p', '?s']]], 
				      $auth);
    }

    if ($self->{SUB_TOPIC_REQ}) {
	$bookmarkshunObject->{-hasTopic} = $self->{SUB_TOPIC_REQ};
	push (@titles, "subtopics of $bookmarkshunObject->{-hasTopic}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::SubTopicQuery, $bookmarkshunObject->{-hasTopic}, 
				      'subtopic details: ', 'found no subtopic ', 
				      [[[['?p', $self->{-hasTopic}, '?o']], ['?p', '?o']]], 
				      $auth);

	push (@titles, "supertopics of $bookmarkshunObject->{-hasTopic}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::SuperTopicQuery, $bookmarkshunObject->{-hasTopic}, 
				      'subtopic details: ', 'found no subtopic ', 
				      [[[['?p', '?s', $self->{-hasTopic}]], ['?p', '?s']]], 
				      $auth);
    }

    if ($self->{REPLY_REQ} || $self->{QUERY_REPLY}) {
	if ($self->{QUERY_REPLY}) {
	    $self->{QUERY_REPLY} = $self->urlize($self->{QUERY_REPLY}, '/reply/');
	    $bookmarkshunObject->{-reply} = $self->{QUERY_REPLY};
	} else {
	    $self->{REPLY_REQ} = $self->urlize($self->{REPLY_REQ}, undef);
	    $bookmarkshunObject->{-reply} = $self->{REPLY_REQ};
	}
	push (@titles, "reply $bookmarkshunObject->{-reply}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::ReplyQuery, $bookmarkshunObject->{-reply}, 
				      'reply details: ', 'found no reply matching ', 
				      [[[['?p', $self->{-reply}, '?o']], ['?p', '?o']]], 
				      $auth);
    }
    if ($self->{QUERY_ATTRIBUTION}) {
	$self->{QUERY_ATTRIBUTION} = $self->urlize($self->{QUERY_ATTRIBUTION}, '/attribution/');
	$bookmarkshunObject->{-attribution} = $self->{QUERY_ATTRIBUTION};
	push (@titles, "bookmarkshun $bookmarkshunObject->{-attribution}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::DocumentQuery, $bookmarkshunObject->{-attribution}, 
				      'attribution details: ', 'found no attribution matching ', 
				      [[[['?p', '?s', '?o', $self->{-attribution}]], ['?p', '?s', '?o']]], 
				      $auth);
    }
    if ($self->{B_RECALLS}) {
	$self->{B_RECALLS} = $self->urlize($self->{B_RECALLS}, undef);
	my $annotators = new BookmarkshunQueryObject();
	$annotators->{-recalls} = $self->{B_RECALLS};
	my $title = "annotators of $annotators->{-recalls}";
	push (@titles, $title);
	my ($results, $selects, $messages, $statements, $query) = 
	    $annotators->templateReq($self->{RDF_DB}, $AnnotationQueryObject::RecallsQuery);
	$self->{PRESENTER}->printAnnotation($title, $query);
	for (my $i = 0; $i < @$results; $i++) {
	    # For each bookmarkshun...
	    eval {
		my $row = $results->[$i];
		my $bookmarkshun = $row->[0];
		$bookmarkshunObject->{-subject} = $bookmarkshun->getUri;
		$bookmarkshunObject->{-recalls} = $self->{B_RECALLS};

		# Get all arcs from this bookmark.
		$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
					      $AnnotationQueryObject::SubjectQuery, $bookmarkshunObject->{-recalls}, 
					      'a bookmark for ', 'incomplete bookmark ', 
					      [[[['?p', '?s', $self->{-recalls}]], ['?p', '?s']]],
					      $auth);
		# Get all replies with root of this query.
#		$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
#					      $AnnotationQueryObject::RootQuery, $bookmarkshunObject->{-subject}, 
#					      'replies for ', 'no replies for bookmark ', 
#					      [[[['?p', '?s', $self->{-subject}]], ['?p', '?s']]],
#					      $auth);

#		# Add all args from the creator object.
#		my $creator = $row->[1];
#		$bookmarkshunObject->{-subject} = $creator->getUri;
#		$bookmarkshunObject->{-recalls} = $self->{B_RECALLS};
#		$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
#					      $AnnotationQueryObject::SubjectQuery, $bookmarkshunObject->{-subject}, 
#					      'a creator for ', 'found no creator for ', 
#					      [[[['?p', '?s', $self->{-subject}]], ['?p', '?s']]],
#					      $auth);

##		my $creator = $row->[1];
##		$bookmarkshunObject->{-subject} = $creator->getUri;
##		$bookmarkshunObject->{-recalls} = $self->{B_RECALLS};
##		$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
##					      $AnnotationQueryObject::SubjectQuery, $bookmarkshunObject->{-recalls}, 
##					      'a bookmark for ', 'found nothing bookmarking ', 
##					      [[[['?p', '?s', $self->{-recalls}]], ['?p', '?s']]],
##					      $auth);
	    }; if ($@) {
		my $ex;
		if ($ex = &catch('W3C::Util::Exception')) {
		} else {
		    $ex = new W3C::Util::PerlException(-error => $@);
		}
		my $message = $ex->getMessage;
		my $annotStr = $results->[$i][0]->show();
		my $errorString = "Error dumping $annotStr:\n$message\n";
		push (@{$self->{PRE_MESSAGES}}, $errorString);
	    }
	}
    }
    if ($self->{REPLY_TREE}) {
	my $threadLeaves = new BookmarkshunQueryObject();
	$threadLeaves->{-subject} = $self->{REPLY_TREE};
	my $title = "thread leaves of $threadLeaves->{-subject}";
	push (@titles, $title);
	my ($results, $selects, $messages, $statements, $query) = 
	    $threadLeaves->templateReq($self->{RDF_DB}, $AnnotationQueryObject::RootQuery);
	$self->{PRESENTER}->printBookmarkshun($title, $query);
	for (my $i = 0; $i < @$results; $i++) {
	    eval {
		my $row = $results->[$i];
		my $bookmarkshun = $row->[0];
		$bookmarkshunObject->{-subject} = $bookmarkshun->getUri;
		$bookmarkshunObject->{-thread} = $self->{BOOKMARKSHUN_THREAD};
		$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
					      $AnnotationQueryObject::SubjectQuery, $bookmarkshunObject->{-subject}, 
					      'replies for ', 'found nothing replying to ', 
					      [[[['?p', '?s', $self->{-thread}]], ['?p', '?s']]],
					      $auth);
	    }; if ($@) {
		my $ex;
		if ($ex = &catch('W3C::Util::Exception')) {
		} else {
		    $ex = new W3C::Util::PerlException(-error => $@);
		}
		my $message = $ex->getMessage;
		my $annotStr = $results->[$i][0]->show();
		my $errorString = "Error dumping $annotStr:\n$message\n";
		push (@{$self->{PRE_MESSAGES}}, $errorString);
	    }
	}
    }
    if ($self->{ALGAE_QUERY}) {
	push (@titles, "algae query $self->{ALGAE_QUERY}");
	$bookmarkshunObject->algaeReq($self->{RDF_DB}, $self->{PRESENTER}, $self->{ALGAE_QUERY}, $auth);
    }
    if (!@titles) {
	push (@titles, 'W3C bookmark server');
    }
    $self->{PRESENTER}->printHead($self->{PRE_MESSAGES}, join ('; ', @titles));
    $self->{PRESENTER}->printFoot($bookmarkshunObject);
    return $self->{PRESENTER};
}

# Return number of reasons $user can perform $method on $resource

sub checkAuth {
    my ($self, $auth, $method, $document) = @_;
    if ($method eq 'GET' || 
	$method eq 'HEAD' || 
	$method eq 'CONNECT' || 
	$method eq 'TRACE' || 
	$method eq 'OPTIONS') {
	return 1;
    } elsif (($method eq 'POST' || $method eq 'PUT' || 
	      $method eq 'DELETE') && defined $auth) {
	return 1;
    } else {
	return 0;
    }
}

# Check whether $auth is the same as $equiv or if $auth is a known admin id.
# This is usefull for PUT, DELETE and rerun as you only want a user to be
# able to replace, delete, or replay his own sessions.
sub checkEquivOrAdminAuth {
    my ($self, $auth, $equiv) = @_;
    if ($equiv && $auth == $equiv) {
	return 1;
    }
    if ($self->{ADMIN_IDS}{$auth}) {
	return 1;
    }
    return 0;
}

sub failAuth {
    my ($self, $auth, $method, $document) = @_;
    my $message = '';
    if ($auth) {
	my $authString = $auth->getUri;
	$message = "<p><em>$authString</em> does not have rights to <em>$method</em> <em>$document</em></p>";
    } else {
	$message = "<p>un-authenticated users do not have the rights to <em>$method</em> <em>$document</em></p>";
    }
    return $self->htmlReply(401, 'text/html', "Auth Required", $message, {'WWW-Authenticate' => 'Basic realm="su"'});
}

# overload CGIApp::importCGIParms so we can get non-CGI POSTs

sub importCGIParms {
    my ($self) = @_;

    # load a properties file if available
    eval {
	$self->{PROPERTIES} = new W3C::Util::Properties('../../../Conf/bookmark.prop');
	$self->{SELF_URI} = $self->{READ}->uriFromProperties($self->{PROPERTIES}, $SCRIPT_HOME_URI_PROP);
    }; if ($@) {if (my $ex = &catch('W3C::Util::FileNotFoundException')) {
	$self->{PROPERTIES} = new W3C::Util::Properties();
	$self->{SELF_URI} = $self->{READ}->url;
    } else {&throw()}}

    # @@@ temporary hack to deal with clients that POST urlencoded data without CGI parms
    if ($ENV{'REQUEST_METHOD'} eq 'POST' && 
	$ENV{CONTENT_TYPE} eq 'application/x-www-form-urlencoded' && 
	$self->{READ}->getPOST() =~ m/^<\?xml/) {
	$ENV{CONTENT_TYPE} = 'application/xml';
	$self->{RDF_INPUT} = &CGI::unescape($self->{READ}->getPOST());
	# this hack requires the parms be ignore (as they are garbage).
	undef $self->{-flags}{-parmMapping};
	$self->SUPER::importCGIParms;
	return;
    }

    # @@@ temporary hack to deal with clients that submit urlencoded GETs with %3As instead of ':'s
    if ($ENV{'REQUEST_METHOD'} eq 'GET' && 
	$ENV{CONTENT_TYPE} eq 'application/x-www-form-urlencoded' && 
	$self->{READ}->param('w3c_recalls') =~ m/^(\w+)\%3A(.*)$/) {
	$self->{B_RECALLS} = "$1:$2";
	# this hack requires the parms be ignore (as they are garbage).
	return;
    }

    $self->SUPER::importCGIParms;
    if ($ENV{'REQUEST_METHOD'} eq 'PUT') {
	my $putXML = $self->{READ}->getPUT();
	$self->{RDF_INPUT} = $putXML;
	$self->{REPLACE_SOURCE} = $self->{SELF_URI}.$ENV{'PATH_INFO'};
	undef $self->{QUERY_BOOKMARKSHUN};
    } elsif ($ENV{'REQUEST_METHOD'} eq 'DELETE') {
	$self->{DELETE_SOURCE} = $self->{SELF_URI}.$ENV{'PATH_INFO'};
	undef $self->{QUERY_ANNOTATION};
    } elsif ($ENV{'REQUEST_METHOD'} eq 'POST') {
	my $postXML = $self->{READ}->getPOST();
	if ($ENV{CONTENT_TYPE} eq 'text/xml' || 
	    $ENV{CONTENT_TYPE} eq 'application/xml') {
	    $self->{RDF_INPUT} = $postXML;
	} elsif ($ENV{CONTENT_TYPE} eq 'application/x-www-form-urlencoded') {
	} else {
	    &throw(new W3C::Util::Exception(-message => "Don't know how to deal with POSTed mime type $ENV{CONTENT_TYPE}"));
	}
    }
}

sub getSelfUri {
    my ($self)  = @_;
    return $self->{SELF_URI};
}

sub word {
    my ($content) = @_;
    my $contentLength = length ($content);
    print <<EOF
Status: 200
Content-type: text/plain

$content
EOF
    ;
#Content-length: $contentLength
#    exit(0);
}

sub urlize {
    my ($self, $urlString, $typeString) = @_;
    if ($urlString !~ m/^(\~?)(\w+)\:(\/\/([\w\d\-]+))?/) {
	if (defined $typeString) {
	    $urlString = $self->getSelfUri.$typeString.$urlString;
	} else {
	    &throw(new W3C::Util::MalformedUriException(-uri => $urlString));
	}
    }
    $urlString = new URI($urlString)->canonical();
    return $urlString;
}

package W3C::Annotations::cgibin::BookmarkScript::BookmarkHTMLPresenter;
use W3C::Rdf::Atoms qw($RDF_SCHEMA_URI);
use W3C::Annotations::AnnotationApp qw($NS_ANNOTATION $NS_HTTP $NS_DC);

use vars qw($NS_BOOKMARKS);
$NS_BOOKMARKS = 'http://www.w3.org/2002/01/bookmark#';

sub printHead {
    my ($self, $preMessages, $title) = @_;
    $self->SUPER::printHead($preMessages, $title);
    $self->printOK('<pre class="printHead">');
}

sub printFoot {
    my ($self, $bookmarkshunObject) = @_;
    $self->endRdf;
    $self->printOK($self->{SERIALIZED_STRING}.'</pre>');
    my $selfUri = '/bookmarks'; $self->{ENGINE}->getSelfUri;
    my $NS_BOOKMARKS = 'http://www.w3.org/2002/01/bookmark#';
    my $displayBookmarkshunId = $self->{RENDERER}->escapeHTML($bookmarkshunObject->{-bookmarkshun});
    my $displayRecalls = $self->{RENDERER}->escapeHTML($bookmarkshunObject->{-recalls});
    my $displayHasTopic = $self->{RENDERER}->escapeHTML($bookmarkshunObject->{-hasTopic});
    my $displaySubTopic = $self->{RENDERER}->escapeHTML($bookmarkshunObject->{-hasTopic});
    my $displayThread = $self->{RENDERER}->escapeHTML($bookmarkshunObject->{-thread});
    my $displayBodyId = $self->{RENDERER}->escapeHTML($bookmarkshunObject->{-bodyId});
    my $displayName = $bookmarkshunObject->{'authorId'} || 'John Smith';
    my $displayEmail = $bookmarkshunObject->{'email'} || 'mailto:jsmith@example.com';
    my $displayCreated = $bookmarkshunObject->{'createdId'} || gmtime().' GMT';
    my $displayModified = $bookmarkshunObject->{'modifiedId'} || gmtime().' GMT';
    my $displayRdf = $self->{ENGINE}->{OVERRIDE_DISPLAY_RDF};
    if ($displayRdf) {
	$displayRdf = $self->{RENDERER}->escapeHTML($displayRdf);
    } else {
	my $displayBookmarkshunStr = $displayBookmarkshunId ? ' r:about="'.$displayBookmarkshunId.'"' : '';
	my $displayBodyStr = $displayBodyId ? ' r:about="'.$displayBodyId.'"' : '';
	$displayRdf = <<EORDF
<?xml version="1.0"?>
$bookmark::RDF_HEAD
  <r:Description$displayBookmarkshunStr>
    <r:type r:resource='${NS_BOOKMARKS}Bookmark'/>
    <!-- b:appearsIn r:resource='http://example.com/folders/ghoti'/ -->
    <b:hasTopic r:resource='#myTopic'/>
    <!-- dc:creator r:resource='http://example.com/person/one'/ -->
    <dc:creator>$displayName</dc:creator>
    <a:created>$displayCreated</a:created>
    <dc:date>$displayModified</dc:date>
    <dc:title>title 1</dc:title>
    <dc:description>this is one of my favorite sites</dc:description>
    <b:recalls r:resource="http://example.org/some/page.html" />
    <a:context>http://www.w3.org/\#xpointer(/html[1]/body[1]/div[1]/a[1])</a:context>
  <a:body>
   <http:Message$displayBodyStr>
    <http:ContentType>text/html</http:ContentType>
    <http:Body r:parseType="Literal"><html>
 <head>
  <title>a bookmark</title>
 </head>
 <body>
  this is my bookmark
  <RDF xmlns="$RDF_SCHEMA_URI">
  </RDF>
 </body>
</html></http:Body>
   </http:Message>
  </a:body>
  </r:Description>

  <r:Description r:about='#myTopic'>
    <a:created>2002-02-08T13:05</a:created>
    <r:type r:resource='http://www.w3.org/2002/01/bookmark#Topic'/>
    <b:subTopicOf r:resource='#myTopic2'/>
    <dc:title>My Favorite Sites</dc:title>
    <dc:description>My collection of favorite sites</dc:description>
  </r:Description>

  <r:Description r:about='#myTopic2'>
    <a:created>2002-02-22T12:01</a:created>
    <r:type r:resource='http://www.w3.org/2002/01/bookmark#Topic'/>
    <dc:title>My Favorites</dc:title>
    <dc:description>These are some of my favorite things</dc:description>
  </r:Description>

$bookmark::RDF_FOOT
EORDF
    ;
	$displayRdf = $self->{RENDERER}->escapeHTML($displayRdf);
    }

    my $displayRdfUri = $self->{ENGINE}->{RDF_URI} || $RDF_SCHEMA_URI;
    $displayRdfUri = $self->{RENDERER}->escapeHTML($displayRdfUri);

    my $displayAttribution = $self->{ENGINE}->{BASE_URI};
    $displayAttribution = $self->{RENDERER}->escapeHTML($displayAttribution);

    $displayRecalls ||= 'http://example.org/some/page.html';
    my $displayAlgaeQuery = $self->{ENGINE}->{ALGAE_QUERY} || "(ask 
     '((${RDF_SCHEMA_URI}type ?bookmark ${NS_BOOKMARKS}Bookmark) 
       (${NS_BOOKMARKS}recalls ?bookmark ?recalls)
       (${NS_BOOKMARKS}hasTopic ?bookmark http://example.com/taxonomy/opaque-whales)
      ) :collect '(?bookmark ?recalls))";
#       (${NS_BOOKMARKS}recalls ?bookmark $displayRecalls)
#       (${NS_BOOKMARKS}appearsIn ?bookmark ?appearsIn)
#       (${NS_DC}creator ?bookmark ?creator)
#       (${NS_ANNOTATION}E-mail ?creator ?email)
#       (${NS_ANNOTATION}name ?creator ?name)
#       (${NS_DC}date ?bookmark ?date)

    $displayAlgaeQuery = $self->{RENDERER}->escapeHTML($displayAlgaeQuery);

    my $tmp = <<EOF;
  <p>
    Welcome to the W3C Bookmark Server!
  </p>
  <p>
    This tool manipulates <a href="http://www.w3.org/1999/07/13-persistant-RDF-DB.html">persistent RDF data</a> associated with a given resource.
  </p>
  <hr />
  <h2><a name=\"byUri\">Create a Bookmark</a></h2>
  <form enctype=\"application/x-www-form-urlencoded\" method=\"post\" action=\"$selfUri\">
    RDF input area:<br />
    <textarea name=\"post_bookmark\" rows=\"13\" cols=\"100\">$displayRdf</textarea><br />
    <a href="http://www.w3.org/TR/REC-xml#NT-ExternalID">External ID</a> (<a href="http://www.w3.org/1999/07/13-persistant-RDF-DB.html#Attributions">attribution</a> for this RDF and a reference for <a href="http://info.internet.isi.edu/in-notes/rfc/files/rfc2396.txt">relative URIs</a>): <input name=\"w3c_resource\" size=55 value=\"\" /><br />
    append&nbsp;previous&nbsp;version:<input type="checkbox" name="w3c_append" value=\"$bookmark::SUBMIT_APPEND_STR\" />
    URI for <a href=\"http://www.w3.org/TR/REC-rdf-syntax/\#literal\">RDF namespace</a>: <input name=\"w3c_rdfUri\" size=50 value=\"$displayRdfUri\" /><br />
<br />
    <input type=\"submit\" name=\"w3c_submit\" value=\"POST new bookmark\" />
    <input type=\"reset\" value=\"Reset this form\" />
    <input type="checkbox" name="w3c_forceText" value="*" />force text/xml 
    <input type="checkbox" name="w3c_ephemoralDB" value="*" />use ephemoral DB
  </form>
  <hr />
  <h2><a name=\"byUri\">Replace a Bookmark</a></h2>
  <form enctype=\"multipart/form-data\" method=\"post\" action=\"$selfUri\">
    RDF input area:<br />
    <textarea name=\"post_bookmark\" rows=\"13\" cols=\"100\">$displayRdf</textarea><br />
    Bookmark ID: <input name=\"replace_source\" size=55 value=\"$displayBookmarkshunId\" /><br />
    URI for <a href=\"http://www.w3.org/TR/REC-rdf-syntax/\#literal\">RDF namespace</a>: <input name=\"w3c_rdfUri\" size=50 value=\"$displayRdfUri\" /><br />
<br />
    <input type=\"submit\" name=\"w3c_submit\" value=\"replace bookmark\" />
    <input type=\"reset\" value=\"Reset this form\" />
    <input type="checkbox" name="w3c_forceText" value="*" />force text/xml 
    <input type="checkbox" name="w3c_ephemoralDB" value="*" />use ephemoral DB
  </form>
  <hr />
  <h2><a name=\"byUri\">Delete a Bookmark</a></h2>
  <form enctype=\"multipart/form-data\" method=\"post\" action=\"$selfUri\">
    Bookmark ID: <input name=\"replace_source\" size=55 value=\"$displayBookmarkshunId\" /><br />
    URI for <a href=\"http://www.w3.org/TR/REC-rdf-syntax/\#literal\">RDF namespace</a>: <input name=\"w3c_rdfUri\" size=50 value=\"$displayRdfUri\" /><br />
<br />
    <input type=\"submit\" name=\"w3c_submit\" value=\"delete bookmark\" />
    <input type=\"reset\" value=\"Reset this form\" />
    <input type="checkbox" name="w3c_forceText" value="*" />force text/xml 
    <input type="checkbox" name="w3c_ephemoralDB" value="*" />use ephemoral DB
  </form>
  <hr />
  <h2><a name=\"byUri\">Query Bookmarks</a></h2>
  <form method=\"get\" action=\"$selfUri\">
    bookmark to retreive: <input name=\"w3c_bookmark\" size=55 value=\"$displayBookmarkshunId\" /><br />
    uris to check for bookmarks: <input name=\"w3c_recalls\" size=55 value=\"$displayRecalls\" /><br />
    hasTopic: <input name=\"w3c_hasTopic\" size=55 value=\"$displayHasTopic\" /><br />
    subTopic: <input name=\"w3c_subTopic\" size=55 value=\"$displaySubTopic\" /><br />
    <textarea name=\"w3c_algaeQuery\" rows=\"13\" cols=\"100\">$displayAlgaeQuery</textarea><br />
    <input type=\"submit\" name=\"w3c_submit\" value=\"query RDF DB\" />
    <input type=\"reset\" value=\"Reset this form\" />
    <input type="checkbox" name="w3c_forceText" value="*" />force text/xml 
    <input type="checkbox" name="w3c_ephemoralDB" value="*" />use ephemoral DB
  </form>
  <hr />
  <h2><a name=\"otherInfo\">Other sources of information</a></h2>
  <ul>
    <li>the <a href="algae">query</a> page</li>
    <li>
	<form method=GET action=\"$selfUri\">
	Source code browser:<br />
	<input type="checkbox" name="w3c_useColor" value="*" /> <font color=\"red\">i</font><font color=\"orange\">n</font> <font color=\"yellow\">c</font><font color=\"green\">o</font><font color=\"blue\">l</font><font color=\"purple\">o</font><font color=\"red\">r</font> (about 2.5 times as large)<br />
	<input type="checkbox" name="w3c_funcLinks" value="*" /> list of functions with links to their declarations<br />
	<input type="checkbox" name="w3c_funcList" value="*" /> links function from function invocations (a cool tool, but it takes about 10 minutes to generate)<br />
	<input name=\"w3c_showSource\" type=\"submit\" value=\"see the source code\" />
	</form>
	</li>
  </ul>
  <h2><a name=\"toDo\">To Do</a></h2>
  <ul>
    <li>A comprehensive user manual.</li>
    <li>A user manager.</li>
  </ul>
  <hr />
EOF
    ;
    $self->printOK($tmp);

    # done - clean up
    $self->printOK($self->{RENDERER}->end_html."\n");
}

__END__

=head1 NAME

W3C::Annotations::cgibin::annotate - A W3C::Rdf::CGIApp to store and retrieve W3C bookmarks

=head1 SYNOPSIS

  http://<server>/Annotations/bookmark

=head1 DESCRIPTION

The C<bookmark> script provides a CGI server that implements the Annotea (L<http://www.w3.org/2001/Annotea/>) Bookmark procotol. It uses a L<W3C::Rdf::SqlDB> database for persistent storage. Installation and use details are available on the Annotea home page.

This module is used with the W3C::Annotations CPAN module.

=head2 Apache Authentication

This is one of the biggest challenges to installing this script. See L<W3C::Annotations::cgibin::access> for details and trouble shooting tips.

=head1 AUTHOR

Eric Prud'hommeaux <eric@w3.org>

=head1 SEE ALSO

L<W3C::Annotations::AnnotationApp>
L<W3C::Annotations::cgibin::access>

=cut

