#!/usr/bin/perl

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

# $Id: bookmark,v 1.26 2005/02/11 07:52:26 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_DC10 $NS_DC11
				       $NS_PALM $NS_ATTRIBUTIONS);

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

use vars qw($REVISION $VERSION);
$REVISION = '$Id: bookmark,v 1.26 2005/02/11 07:52:26 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#';

use vars qw($NS);
$NS = "ns rdf=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>
ns b=<$NS_BOOKMARKS>
ns a=<http://www.w3.org/2000/10/annotation-ns#>
ns t=<http://www.w3.org/2001/03/thread#>
ns http=<http://www.w3.org/1999/xx/http#>
ns dc0=<http://purl.org/dc/elements/1.0/>
ns dc1=<http://purl.org/dc/elements/1.1/>
";
$AnnotationQueryObject::InputBookmarkQuery = "${NS}ask
     (?bookmark rdf:type b:Bookmark {\\%ATTRIB == <\$self->{-attribution}>}.
      ?bookmark b:hasTopic ?hasTopic.
      ?bookmark b:recalls ?recalls
     )
collect (?bookmark)";

$AnnotationQueryObject::InputReplyQuery = "${NS}ask
     (?bookmark rdf:type t:Reply {\\%ATTRIB == <\$self->{-attribution}>}.
      ?bookmark t:inReplyTo ?bookmarks.
      ?bookmark b:hasTopic ?hasTopic
     )
collect (?bookmark ?bookmarks)";

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

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

$AnnotationQueryObject::FatBookmarkshunQuery = "${NS}ask
     (<\$self->{-bookmarkshun}> rdf:type b:Bookmark.
      <\$self->{-bookmarkshun}> b:recalls ?recalls.
      <\$self->{-bookmarkshun}> b:hasTopic ?hasTopic.
      <\$self->{-bookmarkshun}> dc1:creator ?creator.
      ?creator a:E-mail ?email.
      ?creator a:name ?name.
      <\$self->{-bookmarkshun}> a:created ?created.
      <\$self->{-bookmarkshun}> dc1:date ?date
     )
collect (?recalls ?hasTopic ?creator ?created ?date)";

$AnnotationQueryObject::BodyQuery = "${NS}ask
     (<\$self->{-bodyId}> http:Body ?bodyData {?attrib = \\%ATTRIB}.
      <\$self->{-bodyId}> http:ContentType ?contentType
     )
collect (?bodyData ?contentType ?attrib)";

$AnnotationQueryObject::RecallsQuery = "${NS}ask
     (?bookmark rdf:type b:Bookmark .
      ?bookmark b:recalls <\$self->{-recalls}>
      )
collect (?bookmark)";

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

$AnnotationQueryObject::RootQuery = "${NS}ask
     (?reply rdf:type t:Reply.
      ?reply t:root <\$self->{-subject}>.
      dc1:title ?reply ?title
     )
collect (?reply ?title)";

$AnnotationQueryObject::InReplyToQuery = "${NS}ask
     (?reply rdf:type t:Reply.
      ?reply t:inReplyTo <\$self->{-subject}>
     )
collect (?reply)";


$AnnotationQueryObject::FatBookmarksQuery = "${NS}ask
     (?bookmark rdf:type b:Bookmark.
      ?bookmark b:recalls \$self->{-recalls}.
      ?bookmark b:hasTopic ?hasTopic.
      ?bookmark dc1:creator ?creator.
      ?creator a:E-mail ?email.
      ?creator a:name ?name.
      ?bookmark a:created ?created.
      ?bookmark dc1:date ?date
     )
collect (?bookmark ?hasTopic ?creator ?created ?date)";

$AnnotationQueryObject::DocumentQuery = "${NS}ask
     (?p ?s ?o  {\\%ATTRIB == <\$self->{-attribution}>}
     )
collect (?p ?s ?o)";

$AnnotationQueryObject::AttributionQuery = "${NS}ask
     (<\$self->{-bookmarkshun}> b:recalls ?href {\\%ATTRIB == <\$self->{-attribution}>}
     )
collect (?attrib)";

$AnnotationQueryObject::HasTopicQuery = "${NS}ask
     (?bookmark rdf:type b:Bookmark.
      ?bookmark b:recalls ?recalls.
      ?bookmark b:hasTopic <\$self->{-hasTopic}>
     )
collect (?bookmark ?recalls)";

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

$AnnotationQueryObject::SuperTopicQuery = "${NS}ask
     (<\$self->{-hasTopic}> b:subTopicOf ?topic.
     )
collect (?topic)";

#$AnnotationQueryObject::AttributionQuery = "(ask
#     '((rdf:type <\$self->{-bookmarkshun}> b:Bookmark ?r ?b ?attrib) 
#       (b: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_DC11"
       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 {
    local($SIG{"__DIE__"}) = \&DieHandler;
    $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 ($@) {
	my $msg = "failed to execute $evalMe: $!";
	if (my $ex = &catch('W3C::Util::Exception')) {
	    &throw(new W3C::Util::Exception(-message => $msg, 
					    -chainedException => $ex));
	} else {
	    &throw(new W3C::Util::Exception(-message => $msg));
	}
    }

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

sub execute {
    my ($self) = @_;
    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;
    }

    # get authentication parameters
    my $auth = $self->getAuthenticatedUser();
    if (my $realSession = $self->{READ}->getRealSession()) {
	if ($realSession->getEnv('REQUEST_METHOD') eq 'POST') {
	    if (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')));
		}
	    } else {
		&throw($self->failAuth(undef, 'rerun', $realSession->param('w3c_rerun')));
	    }
	} else {
	    my $selfUri = $self->{WRITE}->url;
	    my $desiredSession = $self->{READ}->getSessionId;
	    my $form = "<form method=\"post\" action=\"$selfUri\" enctype=\"application/x-www-form-urlencoded\">
  session-id: <input name=\"w3c_rerun\" value=\"$desiredSession\" />
</form>
";
	    &throw($self->htmlReply(200, 'text/html', "Post Required", $form, {'WWW-Authenticate' => 'Basic realm="su"'}));
	}
    }

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

    my $assertAttrib = $self->{-atomDictionary}->
	getGroundFactAttribution($self->{-atomDictionary}->getUri($self->{BASE_URI}), undef, $auth, undef);
    my $bookmarkshunObject = new BookmarkshunQueryObject($self->{-atomDictionary}, $assertAttrib, $self->{RDF_DB}, -filter => sub {
	# Add the attributions to each graph we serialize.

	my ($db) = @_;
	eval{
	    $self->addIdStatements($db);
	}; if ($@) {if (my $ex = &catch('W3C::Annotations::UserRecord::UserNotFoundException')) {
	    my $user = $ex->getUsername;
	    my $errorString = "There is no record for user \"$user\".";
	    push (@{$self->{PRE_MESSAGES}}, $errorString);
	} elsif (my $ex = &catch('W3C::Annotations::UserRecord::UserIdException')) {
	    push (@{$self->{PRE_MESSAGES}}, $ex->getMessage);
	} elsif (my $ex = &catch('W3C::Util::Exception')) {
	    &throw($ex);
	} else {&throw(new W3C::Util::PerlException())}}
    });

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

    # 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 > 1) {
	    my $struct = $AnnotationQueryObject::AttributionQuery;
	    &throw($self->htmlReply(500, 
				    'text/html', "Server Error", "More than one bookmark \"$deleteAttribution\" found".
				    $bookmarkshunObject->algaeLink($struct, $algaeURL)));
	}
	if (@$results == 0) {
	    $bookmarkshunObject->{-attribution} = $deleteAttribution;
	    ($results, $selects, $messages, $statements, $query) = 
		$bookmarkshunObject->templateReq($self->{RDF_DB}, $AnnotationQueryObject::ForAttributionQuery);
	    if (@$results == 0) {
		my $struct = $AnnotationQueryObject::ForAttributionQuery;
		&throw($self->htmlReply(404, 
					'text/html', "Not Found", "No bookmark \"$deleteAttribution\" found".
					$bookmarkshunObject->algaeLink($struct, $algaeURL)));
	    }
	}
	my $attribution = (($statements->[0]->getTriples)[0]->getAttributionList->getAllDirect)[0];
	my $storedAuth = $attribution->getAuth;

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

	# Don't delete anything with replies.
	$bookmarkshunObject->{-subject} = $deleteAttribution;
	$bookmarkshunObject->{-attribution} = $attribution->getSource->getUri;
	($results, $selects, $messages, $statements, $query) = 
	    $bookmarkshunObject->templateReq($self->{RDF_DB}, $AnnotationQueryObject::InReplyToQuery);
	if ($method eq 'DELETE' && @$results != 0) {
	    my $struct = $AnnotationQueryObject::InReplyToQuery;
	    &throw($self->htmlReply(409, 
				    'text/html', "Protocol Vaguery", "Declined to delete \"$deleteAttribution\" as it has replies".
				    $bookmarkshunObject->algaeLink($struct, $algaeURL)));
	}

	# Get a new Attribution object with the $deleteAttribution id and
	# clear set to 1. This deletes the old data from that Attribution.
	# $self->{-atomDictionary}->getGroundFactAttribution($document, undef, $storedAuth, undef);
	$self->{RDF_DB}->deleteAttribution($attribution);
	$self->{BASE_URI} = $attribution->getSource->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}, $AnnotationQueryObject::BodyQuery);
	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 = $AnnotationQueryObject::BodyQuery;
	    &throw($self->htmlReply(404, 
				    'text/html', "Not Found", "Unable to locate \"$self->{QUERY_BODY}\" with <pre>$bodyInfo</pre>".
				    $bookmarkshunObject->algaeLink($struct, $algaeURL)));
	}
	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", 
					"go see <a href=\"$ov\">$ov</a>", {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", 
					    "could not find overview page <em>$ov</em>"));
		}
	    }
	}
    }

    if ($self->{RDF_INPUT}) {
	my $tmpDB = new W3C::Rdf::RdfDB(-atomDictionary => $self->{-atomDictionary});
	if ($self->checkAuth($auth, 'POST', $self->getSelfUri) > 0) {
	    eval {
		$self->parse($self->{RDF_INPUT}, $tmpDB, $assertAttrib);
	    }; if ($@) {if (my $ex = &catch('W3C::Util::NoSuchFileException')) {
		return print $self->confFileMissing($ex->getFile);
	    } elsif (my $ex = &catch('W3C::Util::Exception')) {
		push (@{$self->{PRE_MESSAGES}}, split("\n", $ex->toString)); # !!! diff with anno
		$self->{OVERRIDE_DISPLAY_RDF} = $self->{RDF_INPUT};
		# return print $self->unknownException($ex);
	    } else {
		return print $self->unknownException(new W3C::Util::Exception(-message => "perl error: $@"));
	    }}
	} else {
	    &throw($self->failAuth($auth, 'POST', $self->getSelfUri));
	}
	$bookmarkshunObject->{-attribution} ||= $self->{BASE_URI};
	my $found = $bookmarkshunObject->inputReq($tmpDB, $self->{PRESENTER},
						$assertAttrib, $self, 
						$deleteAttribution ? scalar $self->decomposeUid($deleteAttribution) : undef, 
						[[$AnnotationQueryObject::InputBookmarkQuery, 
						  [['bookmark', '-bookmarkshun']]], 
						 [$AnnotationQueryObject::InputReplyQuery, 
						  [['reply', '-reply'], ['body', '-bodyId']]]]);
	# Copy to the persistent store if it meets our policy.
	$self->{RDF_DB}->copyTriples($tmpDB);
	if ($found = 0) {
	    push (@titles, "accept bookmark $bookmarkshunObject->{-bookmarkshun}");
	} elsif ($found = 1) {
	    push (@titles, "accept bookmark $bookmarkshunObject->{-bookmarkshun}");
	} else {
	    push (@titles, "accepted data of unknown type");
	}
    }

    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::POQuery, $bookmarkshunObject->{-bookmarkshun}, 
				      'bookmark details: ', 'found no bookmark matching ', 
				      [$AnnotationQueryObject::POQuery], 
				      $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 ', 
				      [$AnnotationQueryObject::HasTopicQuery], 
				      $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 ', 
				      [$AnnotationQueryObject::SubTopicQuery], 
				      $auth);

	push (@titles, "supertopics of $bookmarkshunObject->{-hasTopic}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::SuperTopicQuery, $bookmarkshunObject->{-hasTopic}, 
				      'subtopic details: ', 'found no subtopic ', 
				      [$AnnotationQueryObject::SuperTopicQuery], 
				      $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 ', 
				      [$AnnotationQueryObject::ReplyQuery], 
				      $auth);
    }
    if ($self->{QUERY_ATTRIBUTION}) {
	$self->{QUERY_ATTRIBUTION} = $self->urlize($self->{QUERY_ATTRIBUTION}, '/attribution/');
	$bookmarkshunObject->{-attribution} = $self->{QUERY_ATTRIBUTION};
	push (@titles, "bookmark $bookmarkshunObject->{-attribution}");
	$bookmarkshunObject->genericReq($self->{RDF_DB}, $self->{PRESENTER}, 
				      $AnnotationQueryObject::DocumentQuery, $bookmarkshunObject->{-attribution}, 
				      'attribution details: ', 'found no attribution matching ', 
				      [$AnnotationQueryObject::DocumentQuery], 
				      $auth);
    }
    if ($self->{B_RECALLS}) {
	$self->{B_RECALLS} = $self->urlize($self->{B_RECALLS}, undef);
	my $annotators = new BookmarkshunQueryObject($self->{-atomDictionary}, $assertAttrib, $self->{RDF_DB});
	$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 bookmark...
	    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 ', 
					      [$AnnotationQueryObject::SubjectQuery],
					      $auth);
	    }; if ($@) {
		my $ex;
		if ($ex = &catch('W3C::Util::Exception')) {
		} else {
		    $ex = new W3C::Util::PerlException(-error => $@);
		}
		my $message = $ex->toString; # getMessage;
		my $annotStr = '-- unknown bookmark --';
		eval {$annotStr = $results->[$i][0]->toString();}; if ($@) {}
		my $errorString = "$ex Exception dumping $i:$annotStr:\n$message\n";
		push (@{$self->{PRE_MESSAGES}}, $errorString);
	    }
	}
    }
    if ($self->{REPLY_TREE}) {
	my $threadLeaves = new AnnotationQueryObject($self->{-atomDictionary}, $assertAttrib, $self->{RDF_DB});
	$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 ', 
					      [$AnnotationQueryObject::SubjectQuery],
					      $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('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_DC11);

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 = $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:type ?bookmark b:Bookmark) 
       (b:recalls ?bookmark ?recalls)
       (b:hasTopic ?bookmark http://example.com/taxonomy/opaque-whales)
      ) :collect '(?bookmark ?recalls))";
#       (b:recalls ?bookmark $displayRecalls)
#       (b:appearsIn ?bookmark ?appearsIn)
#       (dc1:creator ?bookmark ?creator)
#       (a:E-mail ?creator ?email)
#       (a:name ?creator ?name)
#       (dc1: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

