# $Id: XPathScript.pm,v 1.10 2003/01/29 01:35:50 jwalt Exp $ package Apache::AxKit::Language::XPathScript; use strict; use vars qw(@ISA $VERSION $stash ); use Apache; use Apache::File; use XML::XPath 1.00; use XML::XPath::XMLParser; use XML::XPath::Node; use XML::XPath::NodeSet; use XML::Parser; use Apache::AxKit::Provider; use Apache::AxKit::Language; use Apache::AxKit::Cache; use Apache::AxKit::Exception; use Apache::AxKit::CharsetConv; @ISA = 'Apache::AxKit::Language'; $VERSION = '0.05'; sub handler { my $class = shift; my ($r, $xml_provider, $style_provider) = @_; my $xpath = XML::XPath->new(); my $source_tree; my $xml_parser = XML::Parser->new( ErrorContext => 2, Namespaces => $XML::XPath::VERSION < 1.07 ? 1 : 0, ParseParamEnt => 1, ); my $parser = XML::XPath::XMLParser->new(parser => $xml_parser); local $Apache::AxKit::Language::XPathScript::local_ent_handler; if (my $entity_handler = $xml_provider->get_ext_ent_handler()) { # warn "XPathScript: setting entity_handler\n"; $xml_parser->setHandlers( ExternEnt => $entity_handler, ); $Apache::AxKit::Language::XPathScript::local_ent_handler = $entity_handler; } AxKit::Debug(6, "XPathScript: Getting XML Source"); if (my $dom = $r->pnotes('dom_tree')) { # dom_tree is an XML::LibXML DOM $source_tree = $parser->parse($dom->toString); delete $r->pnotes()->{'dom_tree'}; } elsif (my $xml = $r->pnotes('xml_string')) { # warn "Parsing from string : $xml\n"; eval { $source_tree = $parser->parse($xml); }; if ($@) { throw Apache::AxKit::Exception::Error(-text => "Parse of xml_string failed: $@"); } } else { $source_tree = get_source_tree($xml_provider, $parser); } $xpath->set_context($source_tree); my $mtime = $style_provider->mtime(); my $style_key = $style_provider->key(); my $package = get_package_name($style_key); AxKit::Debug(6, "Checking stylesheet mtime: $mtime\n"); if ($stash->{$style_key} && exists($stash->{$style_key}{mtime}) && !$style_provider->has_changed($stash->{$style_key}{mtime}) && check_inc_mtime($stash->{$style_key}{mtime}, $style_provider, $stash->{$style_key}{includes})) { # cached... just exec. AxKit::Debug(7, "Using stylesheet cache\n"); } else { # recompile stylesheet. AxKit::Debug(6, "Recompiling stylesheet: $style_key\n"); compile($package, $style_provider); $stash->{$style_key}{mtime} = get_mtime($class, $style_provider); } no strict 'refs'; my $cv = \&{"$package\::handler"}; local $Apache::AxKit::Language::XPathScript::xp = $xpath; my $t = {}; local $Apache::AxKit::Language::XPathScript::trans = $t; local $Apache::AxKit::Language::XPathScript::style_provider = $style_provider; AxKit::Debug(7, "Running XPathScript script\n"); local $^W; my $rc = Apache::Constants::OK; # Execute the page $rc = $cv->($r, $xpath, $t); if (!$r->pnotes('xml_string') && !$r->dir_config('XPSNoApplyTemplatesOnEmptyOutput')) { # no output? Try apply_templates print Apache::AxKit::Language::XPathScript::Toys::apply_templates(); } # warn "Run\n"; $Apache::AxKit::Language::XPathScript::xp = undef; $Apache::AxKit::Language::XPathScript::trans = undef; $Apache::AxKit::Language::XPathScript::style_provider = undef; # warn "Returning $old_status\n"; return $rc; } sub get_source_tree { my ($provider, $parser) = @_; my $source_tree; AxKit::Debug(7, "XPathScript: reparsing file"); eval { my $fh = $provider->get_fh(); # warn("parsing FH $fh with parser $parser\n"); local $/; my $contents = <$fh>; # warn("FH contains: $contents\n"); $source_tree = $parser->parse($contents); # warn("Parse completed\n"); close($fh); # warn("closed filehandle\n"); }; if ($@) { # warn("parse_fh failed\n"); my $str = $provider->get_strref(); # warn("Got str\n"); $source_tree = $parser->parse($$str); # warn("Got source tree\n"); } AxKit::Debug(7, "XPathScript: Returning parsed source tree"); # warn("get_source_tree = $source_tree\n"); return $source_tree; } sub check_inc_mtime { my ($mtime, $provider, $includes) = @_; my $apache = $provider->apache_request; for my $inc (@$includes) { # warn "Checking mtime for $inc\n"; my $sub = $apache->lookup_uri(AxKit::FromUTF8($inc)); local $AxKit::Cfg = Apache::AxKit::ConfigReader->new($sub); my $inc_provider = Apache::AxKit::Provider->new_style_provider($sub); if ($inc_provider->has_changed($mtime)) { # warn "$inc newer (" . $inc_provider->mtime() . ") than last compile ($mtime) causing recompile\n"; return; } } return 1; } sub extract { my ($provider,$scalar_output) = @_; my $contents; eval { my $fh = $provider->get_fh(); local $/; $contents = <$fh>; }; if ($@) { $contents = ${ $provider->get_strref() }; } my $r = AxKit::Apache->request(); if (my $charset = $r->dir_config('XPathScriptCharset')) { AxKit::Debug(8, "XPS: got charset: $charset"); my $map = Apache::AxKit::CharsetConv->new($charset, "utf-8") || die "No such charset: $charset"; $contents = $map->convert($contents); } my $key = $provider->key(); $stash->{$key}{includes} = []; AxKit::Debug(10, "XPathScript: extracting from '$key' contents: $contents\n"); my $script; my $line = 1; while ($contents =~ /\G(.*?)()/gcs) { last if $1 eq '-->'; $params{$2} = $4; } if (!$params{file}) { die "No matching file attribute in #include at line $line ($key)"; } AxKit::Debug(10, "About to include file $params{file}"); $script .= include_file($params{file}, $provider, $scalar_output); AxKit::Debug(10, "include done"); } else { $contents =~ /\G(.*?)%>/gcs || die "No terminating '%>' after line $line ($key)"; my $perl = $1; $perl =~ s/;?$/;/s; # add on ; if its missing. As in <% $foo = 'Hello' %> $script .= $perl; $line += $perl =~ tr/\n//; } } if ($contents =~ /\G(.*)/gcs) { my ($text) = ($1); $text =~ s/\|/\\\|/g; if ($scalar_output) { $script .= "\$__OUTPUT.=q|$text|;"; } else { $script .= "print q|$text|;"; } } return $script; } sub compile { my ($package, $provider) = @_; my $script = extract($provider); my $eval = join('', 'package ', $package, '; use Apache qw(exit);', 'use XML::XPath::Node;', 'Apache::AxKit::Language::XPathScript::Toys->import;', 'sub handler {', 'my ($r, $xp, $t) = @_;', "\n#line 1 " . $provider->key() . "\n", $script, ";\n", 'return Apache::Constants::OK;', "\n}", ); local $^W; AxKit::Debug(10, "Compiling script:\n$eval\n"); eval $eval; if ($@) { AxKit::Debug(1, "Compilation failed: $@"); throw $@; } } sub include_file { my ($filename, $provider, $script_output, $ignore_cache) = @_; my $key = $provider->key(); unless ($ignore_cache) { # return if already included return '' if grep {$_ eq $filename} @{$stash->{$key}{includes}}; } push @{$stash->{$key}{includes}}, $filename; my $apache = $provider->apache_request; my $sub = $apache->lookup_uri(AxKit::FromUTF8($filename)); local $AxKit::Cfg = Apache::AxKit::ConfigReader->new($sub); my $inc_provider = Apache::AxKit::Provider->new_style_provider($sub); return extract($inc_provider, $script_output); } sub XML::XPath::Function::document { # warn "Document function called\n"; return unless $Apache::AxKit::Language::XPathScript::local_ent_handler; my $self = shift; my ($node, @params) = @_; die "document: Function takes 1 parameter\n" unless @params == 1; my $xml_parser = XML::Parser->new( ErrorContext => 2, Namespaces => $XML::XPath::VERSION < 1.07 ? 1 : 0, # ParseParamEnt => 1, ); my $parser = XML::XPath::XMLParser->new(parser => $xml_parser); my $results = XML::XPath::NodeSet->new(); my $uri = $params[0]; my $newdoc; if ($uri =~ /^axkit:/) { $newdoc = $parser->parse( AxKit::get_axkit_uri($uri) ); } elsif ($uri =~ /^xmldb:/) { $newdoc = $parser->parse( Apache::AxKit::Provider::XMLDB::get_xmldb_uri($uri) ); } elsif ($uri =~ /^\w\w+:/) { # assume it's scheme://foo uri eval { # warn "Trying to parse $params[0]\n"; $newdoc = $parser->parse( $Apache::AxKit::Language::XPathScript::local_ent_handler->( undef, undef, $uri ) ); # warn "Parsed OK into $newdoc\n"; }; if (my $E = $@) { if ($E->isa('Apache::AxKit::Exception::IO')) { AxKit::Debug(2, $E); } else { throw Apache::AxKit::Exception::Error(-text => "Parse of '$uri' failed: $E"); }; } } else { AxKit::Debug(3, "Parsing local: $uri\n"); # create a subrequest, so we get the right AxKit::Cfg for the URI my $apache = AxKit::Apache->request; my $sub = $apache->lookup_uri(AxKit::FromUTF8($uri)); local $AxKit::Cfg = Apache::AxKit::ConfigReader->new($sub); my $provider = Apache::AxKit::Provider->new_content_provider($sub); $newdoc = get_source_tree($provider, $parser); undef $provider; undef $apache; undef $sub; } $results->push($newdoc) if $newdoc; #AxKit::Debug(8, "XPathScript: document() returning"); return $results; } sub get_mtime { my $class = shift; my ($provider) = @_; # warn "get_mtime\n"; my $mtime = $provider->mtime(); my $filename = $provider->key(); # warn "mtime: $filename = $mtime\n"; if (!$stash->{$filename}) { # compile stylesheet compile(get_package_name($filename), $provider); $stash->{$filename}{mtime} = $mtime; return 0; } my $apache = $provider->apache_request; for my $inc (@{$stash->{$filename}{includes}}) { my $sub = $apache->lookup_uri(AxKit::FromUTF8($inc)); local $AxKit::Cfg = Apache::AxKit::ConfigReader->new($sub); my $inc_provider = Apache::AxKit::Provider->new_style_provider( $sub, # uri => $inc, ); # warn "Checking mtime of $inc\n"; if ($inc_provider->has_changed($mtime)) { $mtime = $inc_provider->mtime(); } } # warn "returning $mtime\n"; return $mtime; } sub get_package_name { my $filename = shift; # Escape everything into valid perl identifiers $filename =~ s/([^A-Za-z0-9_\/])/sprintf("_%2x",unpack("C",$1))/eg; # second pass cares for slashes and words starting with a digit $filename =~ s{ (/+) # directory (\d?) # package's first character }[ "::" . (length $2 ? sprintf("_%2x",unpack("C",$2)) : "") ]egx; return "Apache::AxKit::Language::XPathScript::ROOT$filename"; } { package Apache::AxKit::Language::XPathScript::Toys; use XML::XPath::Node; use Apache::AxKit::Exception; use vars '@ISA', '@EXPORT'; use Exporter; @ISA = ('Exporter'); @EXPORT = qw( findnodes findvalue findvalues findnodes_as_string apply_templates matches set_namespace import_template DO_SELF_AND_KIDS DO_SELF_ONLY DO_NOT_PROCESS ); sub DO_SELF_AND_KIDS () { return 1; } sub DO_SELF_ONLY () { return -1; } sub DO_NOT_PROCESS () { return 0; } sub MAX_DEPTH () { return 32; } sub import_template { my ($filename, $local_changes) = @_; my ($script) = Apache::AxKit::Language::XPathScript::include_file($filename,$Apache::AxKit::Language::XPathScript::style_provider, 1, 1); # changes may be local to this imported template, or global (default). my ($setup_t); if ($local_changes) { $setup_t = 'local $Apache::AxKit::Language::XPathScript::trans = clone($Apache::AxKit::Language::XPathScript::trans);'; } $script = join('', 'use strict;', 'sub { ', 'my ($node, $real_local_t) = @_;', 'local $Apache::AxKit::Language::XPathScript::xp = $node;', $setup_t, 'my ($t) = $Apache::AxKit::Language::XPathScript::trans;', 'my ($__OUTPUT);', $script,';', '$real_local_t->{pre} = $__OUTPUT;', 'return -1;', '}'); return eval($script); } sub findnodes { $Apache::AxKit::Language::XPathScript::xp->findnodes(@_); } sub findvalue { $Apache::AxKit::Language::XPathScript::xp->findvalue(@_); } sub findvalues { my @nodes = findnodes(@_); map { findvalue('.', $_) } @nodes; } sub findnodes_as_string { $Apache::AxKit::Language::XPathScript::xp->findnodes_as_string(@_); } sub matches { $Apache::AxKit::Language::XPathScript::xp->matches(@_); } sub set_namespace { eval { $Apache::AxKit::Language::XPathScript::xp->set_namespace(@_); }; if ($@) { AxKit::Debug(3, "set_namespace failed: $@"); } } # quieten warnings when compiling this module sub apply_templates (;$@); sub apply_templates (;$@) { unless (@_) { return apply_templates(findnodes('/')); } my ($arg1, @args) = @_; if (!ref($arg1)) { # called with a path to find # warn "apply_templates with path '$arg1'\n"; $arg1 = findnodes($arg1, @args); # return apply_templates($nodes); } my $retval = ''; if (ref($arg1) eq "HASH") { # warn "apply_templates with a hash\n"; local $Apache::AxKit::Language::XPathScript::trans = $arg1; return apply_templates(@args); } elsif ($arg1->isa('XML::XPath::NodeSet')) { # warn "apply_templates with a NodeSet\n"; foreach my $node ($arg1->get_nodelist) { $retval .= translate_node($node); } } else { # warn "apply_templates with a list of " , 1 + @args, " nodes? : ", ref($arg1), "\n"; $retval .= translate_node($arg1); foreach my $node (@args) { $retval .= translate_node($node); } } return $retval; } sub _apply_templates { my @nodes = @_; my $retval = ''; foreach my $node (@nodes) { $retval .= translate_node($node); } return $retval; } sub translate_node { my $node = shift; local $^W; my $translations = $Apache::AxKit::Language::XPathScript::trans; if ($node->isTextNode) { my $trans = $translations->{'text()'}; if (!$trans) { return $node->toString; } if (my $code = $trans->{testcode}) { my $t = {}; my $retval = $code->($node, $t); if ($retval && %$t) { foreach my $tkey (keys %$t) { $trans->{$tkey} = $t->{$tkey}; } } } return $trans->{pre} . $node->toString . $trans->{post}; } if (!$node->isElementNode) { # don't output top-level PI's if ($node->isPINode) { my $retstring = eval { if ($node->getParentNode->getParentNode) { return $node->toString; } return ''; }; return $retstring || ''; } return $node->toString; } # warn "translate_node: ", $node->getName, "\n"; my $node_name = $node->getName; my $trans = $translations->{$node_name}; if (!$trans) { $node_name = '*'; $trans = $translations->{$node_name}; } if (!$trans) { # warn "Default trans\n"; if (my @children = $node->getChildNodes) { return start_tag($node) . _apply_templates(@children) . end_tag($node); } else { return empty_tag($node); } } local $^W; my $dokids = 1; my $search; my $testcode_output = 0; my $t = {}; if ($trans->{testcode}) { # warn "eval testcode\n"; $testcode_output = 1; my $result; my $testcode = $trans->{testcode}; my $depth = 0; while (1) { $result = $testcode->($node, $t); # warn "Testcode returned: $result\n"; if (defined($t->{testcode}) && ref($t->{testcode}) eq "CODE") { if ($depth++ > MAX_DEPTH) { die "Max Depth of ", MAX_DEPTH, " reached on testcode eval!"; } $testcode = $t->{testcode}; $t = {}; } else { last; } } # warn "Here with $result\n"; if ($result !~ /^-?\d+$/) { $dokids = 0; $search = $result; } elsif ($result == DO_NOT_PROCESS) { # don't process anything. return; } elsif ($result == DO_SELF_ONLY) { # -1 means don't do children. $dokids = 0; } elsif ($result == DO_SELF_AND_KIDS) { # do kids } # warn "Here with dokids => $dokids, search => $search\n"; } local $translations->{$node_name}; # copy old values in %{$translations->{$node_name}} = %$trans; if (%$t) { foreach my $key (keys %$t) { $translations->{$node_name}{$key} = $t->{$key}; } $trans = $translations->{$node_name}; } # default: process children too. my $pre = interpolate($node, $trans->{pre}, $testcode_output) . ($trans->{showtag} ? start_tag($node) : '') . interpolate($node, $trans->{prechildren}, $testcode_output); my $post = interpolate($node, $trans->{postchildren}, $testcode_output) . ($trans->{showtag} ? end_tag($node) : '') . interpolate($node, $trans->{post}, $testcode_output); if ($dokids) { my $middle = ''; for my $kid ($node->getChildNodes()) { if ($kid->isElementNode) { $middle .= interpolate($node, $trans->{prechild}) . _apply_templates($kid) . interpolate($node, $trans->{postchild}); } else { $middle .= _apply_templates($kid); } } return $pre . $middle . $post; } elsif ($search) { my $middle = ''; for my $kid (findnodes($search, $node)) { if ($kid->isElementNode) { $middle .= interpolate($node, $trans->{prechild}) . _apply_templates($kid) . interpolate($node, $trans->{postchild}); } else { $middle .= _apply_templates($kid); } } return $pre . $middle . $post; } else { return $pre . $post; } } sub start_tag { my ($node) = @_; my $name = $node->getName; return '' unless $name; my $string = "<" . $name; foreach my $ns ($node->getNamespaceNodes) { $string .= $ns->toString; } foreach my $attr ($node->getAttributeNodes) { $string .= $attr->toString; } $string .= ">"; return $string; } sub end_tag { my ($node) = @_; if (my $name = $node->getName) { return ""; } else { return ''; } } sub empty_tag { my ($node) = @_; my $name = $node->getName; return '' unless $name; my $string = "<" . $name; foreach my $ns ($node->getNamespaceNodes) { $string .= $ns->toString; } foreach my $attr ($node->getAttributeNodes) { $string .= $attr->toString; } $string .= " />"; return $string; } sub interpolate { my ($node, $string, $ignore) = @_; return $string if $XPathScript::DoNotInterpolate || $ignore; return $string unless AxKit::Apache->request->dir_config('AxXPSInterpolate'); my $new = ''; while ($string =~ m/\G(.*?)\{(.+?)\}/gcs) { my ($pre, $path) = ($1, $2); $new .= $pre; $new .= $node->findvalue($path); } $string =~ /\G(.*)/gcs; $new .= $1 if defined $1; return $new; } # make a clone, but copy subs. sub clone { my ($a) = @_; my ($b); if (ref($a) eq "HASH") { $b = {}; foreach my $key (keys(%$a)) { my ($copy) = clone($a->{$key}); $b->{$key} = $copy; } } else { # copy as is $b = $a; } return $b; } 1; } 1; __END__ =head1 NAME Apache::AxKit::Language::XPathScript - An XML Stylesheet Language =head1 SYNOPSIS AxAddStyleMap "application/x-xpathscript => \ Apache::AxKit::Language::XPathScript" =head1 DESCRIPTION This documentation has been removed. The definitive reference for XPathScript is now at http://axkit.org/docs/xpathscript/guide.dkb in DocBook format. =cut