# $Id: XPath.pm,v 1.10 2004/11/16 04:38:52 asc Exp $
use strict;
package Apache::XPointer::XPath;
use base qw (Apache::XPointer);
$Apache::XPointer::XPath::VERSION = '1.1';
=head1 NAME
Apache::XPointer::XPath - mod_perl handler to address XML fragments using XPath.
=head1 SYNOPSIS
SetHandler perl-script
PerlHandler Apache::XPointer::XPath
PerlSetVar XPointerSendRangeAs "multipart/mixed"
#
my $ua = LWP::UserAgent->new();
my $req = HTTP::Request->new(GET => "http://example.com/foo/bar/baz.xml");
$req->header("Range" => qq(xmlns("x=x-urn:example")xpointer(*//x:thingy)));
$req->header("Accept" => qq(application/xml, multipart/mixed));
my $res = $ua->request($req);
=head1 DESCRIPTION
Apache::XPointer is a mod_perl handler to address XML fragments using
the HTTP 1.1 I and I headers and the XPath scheme, as described
in the paper : I.
Additionally, the handler may also be configured to recognize a conventional
CGI parameter as a valid range identifier.
If no 'range' property is found, then the original document is
sent unaltered.
If an I header is specified with no corresponding match, then the
server will return (406) HTTP_NOT_ACCEPTABLE.
Successful queries will return (206) HTTP_PARTIAL_CONTENT.
=head1 OPTIONS
=head2 XPointerSendRangeAs
Return matches as one of the following content-types :
=over 4
=item * B
--match
Content-type: text/xml; charset=UTF-8
hello
--match
Content-type: text/xml; charset=UTF-8
world
--match--
=item * B
hello
world
=back
I
=head2 XPointerAllowCGI
If set to B then the handler will check for CGI parameters as well
as HTTP headers. CGI parameters are checked only if no matching HTTP
header is present.
Case insensitive.
=head2 XPointerCGIRangeParam
The name of the CGI parameter to check for an XPath range.
Default is B
=head2 XPointerCGIAcceptParam
The name of the CGI parameter to list one or more acceptable
content types for a response.
Default is B
=head1 MOD_PERL COMPATIBILITY
This handler will work with both mod_perl 1.x and mod_perl 2.x.
=cut
use XML::LibXML;
use XML::LibXML::XPathContext;
sub send_as {
my $pkg = shift;
my $as = shift;
if ($as eq "multipart/mixed") {
return "send_multipart";
}
elsif ($as eq "application/xml") {
return "send_xml";
}
else {
return undef;
}
}
sub parse_range {
my $pkg = shift;
my $apache = shift;
my $range = shift;
my %ns = ();
my $pointer = undef;
$range =~ s/^\s+//;
$range =~ s/\s+$//;
# FIX ME - hooks to deal with '^' escaped
# parens per the XPointer spec
while ($range =~ /\G\s*xmlns\(([^=]+)=([^\)]+)\)/mg) {
$ns{ $1 } = $2;
}
$range =~ /xpointer\((.*)\)$/;
$pointer = $1;
return {query => $pointer,
ns => \%ns };
}
sub query {
my $pkg = shift;
my $apache = shift;
my $args = shift;
my $parser = XML::LibXML->new();
my $doc = undef;
eval {
$doc = $parser->parse_file($apache->filename());
};
if ($@) {
$apache->log()->error(sprintf("failed to parse file '%s', %s",
$apache->filename(),$@));
return {success => 0,
response => $pkg->_server_error()};
}
my $context = XML::LibXML::XPathContext->new($doc);
my $ns = $args->{'ns'};
foreach my $prefix (keys %$ns) {
$context->registerNs($prefix,$ns->{$prefix});
}
#
my $result = undef;
eval {
$result = $context->findnodes($args->{'query'});
};
if ($@) {
$apache->log()->error(sprintf("failed to find nodes for '%s', %s",
$args->{'query'},$@));
return {success => 0,
response => $pkg->_server_error()};
}
#
return {success => 1,
encoding => $doc->encoding(),
result => $result};
}
sub send_multipart {
my $pkg = shift;
my $apache = shift;
my $res = shift;
foreach my $node ($res->{'result'}->get_nodelist()) {
# note : $node->toString() does not serialize
# namespace information
# $node->toStringC14N() results in : $node's
# root element from being included (I'm sure
# there's magic XPath to deal with this but
# I haven't figured it out yet; mal-formed
# XML
my $root = XML::LibXML::Element->new($node->localname());
$root->setNamespace($node->namespaceURI(),
$node->prefix());
foreach my $child ($node->childNodes()) {
# see also : libxml/tree.h
# XML_ELEMENT_NODE= 1
if ($child->nodeType() == 1) {
$root->setNamespace($child->namespaceURI(),
$child->prefix());
}
$root->addChild($child);
}
$apache->print(qq(--match\n));
$apache->print(sprintf("Content-type: text/xml; charset=%s\n\n",$res->{'encoding'}));
$apache->print($root->toString(1,1));
$apache->print(qq(\n));
}
$apache->print(qq(--match--\n));
return 1;
}
sub send_xml {
my $pkg = shift;
my $apache = shift;
my $res = shift;
# Note : the document-ness of $doc handles
# all the goofy XMLNS hoops we jump
# through above
my $doc = XML::LibXML::Document->new();
$doc->setEncoding($res->{'encoding'});
my $root = XML::LibXML::Element->new("range");
$root->setNamespace("x-urn:cpan:ascope:apache-xpointer-xpath#","xp");
foreach my $node ($res->{'result'}->get_nodelist()) {
my $item = XML::LibXML::Element->new("xp:match");
$item->addChild($node);
$root->addChild($item);
}
$doc->setDocumentElement($root);
#
$apache->print($doc->toString());
return 1;
}
=head1 VERSION
1.1
=head1 DATE
$Date: 2004/11/16 04:38:52 $
=head1 AUTHOR
Aaron Straup Cope Eascope@cpan.orgE
=head1 SEE ALSO
L
=head1 LICENSE
Copyright (c) 2004 Aaron Straup Cope. All rights reserved.
This is free software, you may use it and distribute it under
the same terms as Perl itself.
=cut
return 1;