# xpath.rb -- XPath implementation for Ruby, including write access
# Copyright (C) 2004,2005 Olaf Klischat
require 'rexml/document'
module XML
class XXPathError < RuntimeError
end
# Instances of this class hold (in a pre-compiled form) an XPath
# pattern. You call instance methods like +each+, +first+, +all+,
# create_new on instances of this class to apply the
# pattern to REXML elements.
class XXPath
# create and compile a new XPath. _xpathstr_ is the string
# representation (XPath pattern) of the path
def initialize(xpathstr)
@xpathstr = xpathstr # for error messages
xpathstr=xpathstr[1..-1] if xpathstr[0]==?/
# TODO: avoid code duplications
# maybe: build & create the procs using eval
@creator_procs = [ proc{|node,create_new| node} ]
@reader_proc = proc {|nodes| nodes}
xpathstr.split('/').reverse.each do |part|
prev_creator = @creator_procs[-1]
prev_reader = @reader_proc
case part
when /^(.*?)\[@(.*?)='(.*?)'\]$/
name,attr_name,attr_value = [$1,$2,$3]
@creator_procs << curr_creator = proc {|node,create_new|
prev_creator.call(Accessors.create_subnode_by_name_and_attr(node,create_new,
name,attr_name,attr_value),
create_new)
}
@reader_proc = proc {|nodes|
next_nodes = Accessors.subnodes_by_name_and_attr(nodes,
name,attr_name,attr_value)
if (next_nodes == [])
throw :not_found, [nodes,curr_creator]
else
prev_reader.call(next_nodes)
end
}
when /^(.*?)\[(.*?)\]$/
name,index = [$1,$2.to_i]
@creator_procs << curr_creator = proc {|node,create_new|
prev_creator.call(Accessors.create_subnode_by_name_and_index(node,create_new,
name,index),
create_new)
}
@reader_proc = proc {|nodes|
next_nodes = Accessors.subnodes_by_name_and_index(nodes,
name,index)
if (next_nodes == [])
throw :not_found, [nodes,curr_creator]
else
prev_reader.call(next_nodes)
end
}
when /^@(.*)$/
name = $1
@creator_procs << curr_creator = proc {|node,create_new|
prev_creator.call(Accessors.create_subnode_by_attr_name(node,create_new,name),
create_new)
}
@reader_proc = proc {|nodes|
next_nodes = Accessors.subnodes_by_attr_name(nodes,name)
if (next_nodes == [])
throw :not_found, [nodes,curr_creator]
else
prev_reader.call(next_nodes)
end
}
when '*'
@creator_procs << curr_creator = proc {|node,create_new|
prev_creator.call(Accessors.create_subnode_by_all(node,create_new),
create_new)
}
@reader_proc = proc {|nodes|
next_nodes = Accessors.subnodes_by_all(nodes)
if (next_nodes == [])
throw :not_found, [nodes,curr_creator]
else
prev_reader.call(next_nodes)
end
}
else
name = part
@creator_procs << curr_creator = proc {|node,create_new|
prev_creator.call(Accessors.create_subnode_by_name(node,create_new,name),
create_new)
}
@reader_proc = proc {|nodes|
next_nodes = Accessors.subnodes_by_name(nodes,name)
if (next_nodes == [])
throw :not_found, [nodes,curr_creator]
else
prev_reader.call(next_nodes)
end
}
end
end
end
# loop over all sub-nodes of _node_ that match this XPath.
def each(node,options={},&block)
all(node,options).each(&block)
end
# the first sub-node of _node_ that matches this XPath. If nothing
# matches, raise XXPathError unless :allow_nil=>true was provided.
#
# If :ensure_created=>true is provided, first() ensures that a
# match exists in _node_, creating one if none existed before.
#
# path.first(node,:create_new=>true) is equivalent
# to path.create_new(node).
def first(node,options={})
a=all(node,options)
if a.empty?
if options[:allow_nil]
nil
else
raise XXPathError, "path not found: #{@xpathstr}"
end
else
a[0]
end
end
# Return an Enumerable with all sub-nodes of _node_ that match
# this XPath. Returns an empty Enumerable if no match was found.
#
# If :ensure_created=>true is provided, all() ensures that a match
# exists in _node_, creating one (and returning it as the sole
# element of the returned enumerable) if none existed before.
def all(node,options={})
raise "options not a hash" unless Hash===options
if options[:create_new]
return [ @creator_procs[-1].call(node,true) ]
else
last_nodes,rest_creator = catch(:not_found) do
return @reader_proc.call([node])
end
if options[:ensure_created]
[ rest_creator.call(last_nodes[0],false) ]
else
[]
end
end
end
# create a completely new match of this XPath in
# base_node. "Completely new" means that a new node will be
# created for each path element, even if a matching node already
# existed in base_node.
#
# path.create_new(node) is equivalent to
# path.first(node,:create_new=>true).
def create_new(base_node)
first(base_node,:create_new=>true)
end
module Accessors #:nodoc:
# we need a boolean "unspecified?" attribute for XML nodes --
# paths like "*" oder (somewhen) "foo|bar" create "unspecified"
# nodes that the user must then "specify" by setting their text
# etc. (or manually setting unspecified=false)
#
# This is mixed into the REXML::Element and
# XML::XXPath::Accessors::Attribute classes.
module UnspecifiednessSupport
def unspecified?
@xml_xpath_unspecified ||= false
end
def unspecified=(x)
@xml_xpath_unspecified = x
end
def self.included(mod)
mod.module_eval <<-EOS
alias_method :_text_orig, :text
alias_method :_textis_orig, :text=
def text
# we're suffering from the "fragile base class"
# phenomenon here -- we don't know whether the
# implementation of the class we get mixed into always
# calls text (instead of just accessing @text or so)
if unspecified?
"[UNSPECIFIED]"
else
_text_orig
end
end
def text=(x)
_textis_orig(x)
self.unspecified=false
end
alias_method :_nameis_orig, :name=
def name=(x)
_nameis_orig(x)
self.unspecified=false
end
EOS
end
end
class REXML::Element #:nodoc:
include UnspecifiednessSupport
end
# attribute node, half-way compatible
# with REXML's Element.
# REXML doesn't provide one...
#
# The all/first calls return instances of this class if they
# matched an attribute node.
class Attribute
attr_reader :parent, :name
attr_writer :name
def initialize(parent,name)
@parent,@name = parent,name
end
def self.new(parent,name,create)
if parent.attributes[name]
super(parent,name)
else
if create
parent.attributes[name] = "[unset]"
super(parent,name)
else
nil
end
end
end
# the value of the attribute.
def text
parent.attributes[@name]
end
def text=(x)
parent.attributes[@name] = x
end
def ==(other)
other.kind_of?(Attribute) and other.parent==parent and other.name==name
end
include UnspecifiednessSupport
end
# read accessors
for things in %w{name name_and_attr name_and_index attr_name all} do
self.module_eval <<-EOS
def self.subnodes_by_#{things}(nodes, *args)
nodes.map{|node| subnodes_by_#{things}_singlesrc(node,*args)}.flatten
end
EOS
end
def self.subnodes_by_name_singlesrc(node,name)
node.elements.select{|elt| elt.name==name}
end
def self.subnodes_by_name_and_attr_singlesrc(node,name,attr_name,attr_value)
node.elements.select{|elt| elt.name==name and elt.attributes[attr_name]==attr_value}
end
def self.subnodes_by_name_and_index_singlesrc(node,name,index)
index-=1
byname=subnodes_by_name_singlesrc(node,name)
if index>=byname.size
[]
else
[byname[index]]
end
end
def self.subnodes_by_attr_name_singlesrc(node,name)
attr=Attribute.new(node,name,false)
if attr then [attr] else [] end
end
def self.subnodes_by_all_singlesrc(node)
node.elements.to_a
end
# write accessors
# precondition: unless create_new, we know that a node with
# exactly the requested attributes doesn't exist yet (else we
# wouldn't have been called)
def self.create_subnode_by_name(node,create_new,name)
node.elements.add name
end
def self.create_subnode_by_name_and_attr(node,create_new,name,attr_name,attr_value)
if create_new
newnode = node.elements.add(name)
else
newnode = subnodes_by_name_singlesrc(node,name)[0]
if not(newnode) or newnode.attributes[attr_name]
newnode = node.elements.add(name)
end
end
newnode.attributes[attr_name]=attr_value
newnode
end
def self.create_subnode_by_name_and_index(node,create_new,name,index)
name_matches = subnodes_by_name_singlesrc(node,name)
if create_new and (name_matches.size >= index)
raise XXPathError, "XPath (#{@xpathstr}): #{name}[#{index}]: create_new and element already exists"
end
newnode = name_matches[0]
(index-name_matches.size).times do
newnode = node.elements.add name
end
newnode
end
def self.create_subnode_by_attr_name(node,create_new,name)
if create_new and node.attributes[name]
raise XXPathError, "XPath (#{@xpathstr}): @#{name}: create_new and attribute already exists"
end
Attribute.new(node,name,true)
end
def self.create_subnode_by_all(node,create_new)
node = node.elements.add
node.unspecified = true
node
end
end
end
end