diff --git a/lib/rexml/doctype.rb b/lib/rexml/doctype.rb
index f58a3929..a52ac057 100644
--- a/lib/rexml/doctype.rb
+++ b/lib/rexml/doctype.rb
@@ -113,27 +113,26 @@ def node_type
end
def attributes_of element
- attribute_declarations_of(element).map do |key, value|
+ raw_attributes = _attlist_mappings[element] || {}
+ raw_attributes.map do |key, value|
Attribute.new(key, value)
end
end
def attribute_of element, attribute
- attribute_declarations_of(element)[attribute]
+ _attlist_mappings[element]&.[](attribute)
end
- def attribute_declarations_of(name)
- raw_attributes = {}
- decls = select do |child|
- child.kind_of?(AttlistDecl) && child.element_name == name
- end
- decls.each do |child|
+ def _attlist_mappings # :nodoc:
+ mapping = {}
+ grep(AttlistDecl).each do |child|
+ raw_attributes = mapping[child.element_name] ||= {}
child.each do |key, val|
# First declaration wins
raw_attributes[key] = val unless raw_attributes.key? key
end
end
- raw_attributes
+ mapping
end
def clone
diff --git a/lib/rexml/document.rb b/lib/rexml/document.rb
index d5db713d..bcf612fb 100644
--- a/lib/rexml/document.rb
+++ b/lib/rexml/document.rb
@@ -449,19 +449,6 @@ def document
private
- attr_accessor :namespaces_cache
-
- # New document level cache is created and available in this block.
- # This API is thread unsafe. Users can't change this document in this block.
- def enable_cache
- @namespaces_cache = {}
- begin
- yield
- ensure
- @namespaces_cache = nil
- end
- end
-
def build( source )
Parsers::TreeParser.new( source, self ).parse
end
diff --git a/lib/rexml/element.rb b/lib/rexml/element.rb
index 8f417c6e..a002db07 100644
--- a/lib/rexml/element.rb
+++ b/lib/rexml/element.rb
@@ -588,12 +588,7 @@ def prefixes
# d.elements['//c'].namespaces # => {"x"=>"1", "y"=>"2", "z"=>"3"}
#
def namespaces
- namespaces_cache = document&.__send__(:namespaces_cache)
- if namespaces_cache
- namespaces_cache[self] ||= calculate_namespaces
- else
- calculate_namespaces
- end
+ _calculate_namespaces
end
# :call-seq:
@@ -618,13 +613,10 @@ def namespaces
#
def namespace(prefix=nil)
if prefix.nil?
- prefix = prefix()
+ _namespace_internal
+ else
+ _namespace_lookup_internal(prefix)
end
- prefix = (prefix == '') ? 'xmlns' : prefix.delete_prefix("xmlns:")
- ns = namespaces[prefix]
-
- ns = '' if ns.nil? and prefix == 'xmlns'
- ns
end
# :call-seq:
@@ -1507,15 +1499,32 @@ def write(output=$stdout, indent=-1, transitive=false, ie_hack=false)
formatter.write( self, output )
end
- private
- def calculate_namespaces
- if parent
- parent.namespaces.merge(attributes.namespaces)
- else
- attributes.namespaces
- end
+ # Returns namespace of the element
+ def _namespace_internal(namespaces = _calculate_namespaces) # :nodoc:
+ _namespace_lookup_internal(prefix, namespaces)
+ end
+
+ # Lookup namespace for the given prefix in the context of the element
+ def _namespace_lookup_internal(prefix, namespaces = _calculate_namespaces) # :nodoc:
+ prefix = (prefix == '') ? 'xmlns' : prefix.delete_prefix("xmlns:")
+ ns = namespaces[prefix]
+ ns = '' if ns.nil? and prefix == 'xmlns'
+ ns
+ end
+
+ def _calculate_namespaces(cache_hash = nil, attlist_mappings = nil) # :nodoc:
+ return cache_hash[self] if cache_hash && cache_hash.key?(self)
+
+ attlist_mappings ||= document&.doctype&._attlist_mappings || {}
+ inherited_namespaces = parent ? parent._calculate_namespaces(cache_hash, attlist_mappings) : {}
+ attr_namespaces = attributes._calculate_namespaces(attlist_mappings)
+ namespaces = attr_namespaces.any? ? inherited_namespaces.merge(attr_namespaces) : inherited_namespaces
+ cache_hash[self] = namespaces if cache_hash
+ namespaces
end
+ private
+
def __to_xpath_helper node
rv = node.expanded_name.clone
if node.parent
@@ -2297,7 +2306,7 @@ def each
# attrs.get_attribute('nosuch') # => nil
#
def get_attribute( name )
- fetch(name, nil) || attlist_attributes&.[](name)
+ fetch(name, nil) || attlist_attributes[name]
end
# :call-seq:
@@ -2372,11 +2381,7 @@ def prefixes
# d.root.attributes.namespaces # => {"xmlns"=>"foo", "x"=>"bar", "y"=>"twee"}
#
def namespaces
- namespaces = {}
- each_effective_attribute do |attribute|
- namespaces[attribute.name] = attribute.value if attribute.namespace_declaration?
- end
- namespaces
+ _calculate_namespaces
end
# :call-seq:
@@ -2507,16 +2512,24 @@ def get_attribute_ns(namespace, name)
end
end
+ def _calculate_namespaces(attlist_mappings = nil) # :nodoc:
+ namespaces = {}
+ each_effective_attribute(attlist_mappings) do |attribute|
+ namespaces[attribute.name] = attribute.value if attribute.namespace_declaration?
+ end
+ namespaces
+ end
+
private
# Iterates over the attributes of the element and those in the DTD.
# If the element has an attribute with the same name as one in the DTD,
# the element's attribute takes precedence.
- def each_effective_attribute
- return to_enum(__method__) unless block_given?
- attr = attlist_attributes
- attr = attr ? attr.merge(self) : self
- attr.each_value do |attribute|
+ def each_effective_attribute(attlist_mappings = nil)
+ return to_enum(__method__, attlist_mappings) unless block_given?
+ attlist = attlist_attributes(attlist_mappings)
+ attributes = attlist.empty? ? self : attlist.merge(self)
+ attributes.each_value do |attribute|
yield attribute
end
end
@@ -2524,16 +2537,16 @@ def each_effective_attribute
alias each_own_attribute each_value
private :each_own_attribute
- def attlist_attributes
- doctype = @element.document&.doctype
- if doctype
- expn = @element.is_a?(Document) ? doctype.name : @element.expanded_name
- doctype.attribute_declarations_of(expn).map do |key, value|
- attr = Attribute.new(key, value)
- attr.element = @element
- [key, attr]
- end.to_h
- end
+ def attlist_attributes(attlist_mappings = nil)
+ attlist_mappings ||= @element.document&.doctype&._attlist_mappings
+ return {} unless attlist_mappings
+
+ expn = @element.is_a?(Document) ? @element.doctype&.name : @element.expanded_name
+ attlist_mappings[expn]&.map do |key, value|
+ attr = Attribute.new(key, value)
+ attr.element = @element
+ [key, attr]
+ end.to_h
end
end
end
diff --git a/lib/rexml/xpath_parser.rb b/lib/rexml/xpath_parser.rb
index 9c856a65..00d066ec 100644
--- a/lib/rexml/xpath_parser.rb
+++ b/lib/rexml/xpath_parser.rb
@@ -63,6 +63,9 @@ def initialize(strict: false)
@namespaces = nil
@variables = {}
@functions = FunctionsClass.new
+ @attlist_mappings = nil
+ @document = nil
+ @element_namespaces_cache = {}
@nest = 0
@strict = strict
end
@@ -85,18 +88,12 @@ def parse path, node
node = node.first
end
- document = node.document
- if document
- document.__send__(:enable_cache) do
- match( path_stack, node )
- end
- else
- match( path_stack, node )
- end
+ match(path_stack, node)
end
def get_first path, node
path_stack = @parser.parse( path )
+ @document = node.document
first( path_stack, node )
end
@@ -152,6 +149,7 @@ def first( path_stack, node )
def match(path_stack, node)
+ @document = node.document
nodeset = [node]
result = expr(path_stack, nodeset)
case result
@@ -167,20 +165,45 @@ def strict?
@strict
end
- # Returns a String namespace for a node, given a prefix
+ # Returns a String namespace for a prefix used in xpath
# The rules are:
#
# 1. Use the supplied namespace mapping first.
- # 2. If no mapping was supplied, use the context node to look up the namespace
- def get_namespace( node, prefix )
+ # 2. If no mapping was supplied, use the context node to look up the namespace as a fallback.
+ def get_xpath_namespace(node, prefix)
if @namespaces
@namespaces[prefix] || ''
+ elsif node.node_type == :element
+ element_namespace_lookup(node, prefix)
else
- return node.namespace( prefix ) if node.node_type == :element
''
end
end
+ # Returns attribute's namespace URI while caching the
+ # intermediate result to speed up retrieval of namespaces
+ def attribute_namespace(attribute)
+ attribute.prefix == '' ? '' : element_namespace_lookup(attribute.element, attribute.prefix)
+ end
+
+ # Return element's namespace URI while caching the
+ # intermediate result to speed up retrieval of namespaces
+ def element_namespace(element)
+ element._namespace_internal(element_namespaces(element))
+ end
+
+ # Returns a hash of namespaces for the given element while caching the
+ # intermediate result to speed up retrieval of namespaces
+ def element_namespaces(element)
+ @attlist_mappings ||= @document&.doctype&._attlist_mappings || {}
+ element._calculate_namespaces(@element_namespaces_cache, @attlist_mappings)
+ end
+
+ # Returns namespace of the prefix in the context of the element,
+ # while caching the intermediate result to speed up retrieval of namespaces
+ def element_namespace_lookup(element, prefix)
+ element._namespace_lookup_internal(prefix, element_namespaces(element))
+ end
# Expr takes a stack of path elements and a set of nodes (either a Parent
# or an Array and returns an Array of matching nodes
@@ -642,20 +665,20 @@ def node_test(path_stack, any_type: :element)
node.name == name
elsif prefix.empty?
if strict?
- node.name == name and node.namespace == ""
+ node.name == name and element_namespace(node) == ""
else
- node.name == name and node.namespace == get_namespace(node, prefix)
+ node.name == name and element_namespace(node) == get_xpath_namespace(node, prefix)
end
else
- node.name == name and node.namespace == get_namespace(node, prefix)
+ node.name == name and element_namespace(node) == get_xpath_namespace(node, prefix)
end
when :attribute
if prefix.nil?
node.name == name
elsif prefix.empty?
- node.name == name and node.namespace == ""
+ node.name == name and attribute_namespace(node) == ""
else
- node.name == name and node.namespace == get_namespace(node.element, prefix)
+ node.name == name and attribute_namespace(node) == get_xpath_namespace(node.element, prefix)
end
else
false
@@ -666,11 +689,11 @@ def node_test(path_stack, any_type: :element)
->(node) do
case node.node_type
when :element
- namespaces = @namespaces || node.namespaces
- node.namespace == namespaces[prefix]
+ namespaces = @namespaces || element_namespaces(node)
+ element_namespace(node) == namespaces[prefix]
when :attribute
- namespaces = @namespaces || node.element.namespaces
- node.namespace == namespaces[prefix]
+ namespaces = @namespaces || element_namespaces(node.element)
+ attribute_namespace(node) == namespaces[prefix]
else
false
end
diff --git a/test/test_namespace.rb b/test/test_namespace.rb
index a41e5058..6d405c9a 100644
--- a/test/test_namespace.rb
+++ b/test/test_namespace.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: false
+require "core_assertions"
module REXMLTests
class TestNamespace < Test::Unit::TestCase
+ include Test::Unit::CoreAssertions
include Helper::Fixture
include REXML
@@ -34,5 +36,25 @@ def test_xml_namespace
assert_equal("http://www.w3.org/XML/1998/namespace",
document.root.namespace("xml"))
end
+
+ def test_deep_element_namespace_linear
+ omit('Recursion too deep on JRuby') if RUBY_ENGINE == "jruby"
+
+ max_depth = 3000
+ xml = <<~XML
+ #{'' * max_depth + '' * max_depth}
+ XML
+ doc = Document.new(xml)
+ prepare_element = ->(depth) do
+ node = doc.root
+ depth.times { node = node.first }
+ node || raise
+ end
+
+ assert_linear_performance([30, 100, 300, 1000, 3000], rehearsal: 10) do |depth|
+ elem = prepare_element.call(depth)
+ elem.namespace
+ end
+ end
end
end
diff --git a/test/xpath/test_base.rb b/test/xpath/test_base.rb
index 911fda12..0383a417 100644
--- a/test/xpath/test_base.rb
+++ b/test/xpath/test_base.rb
@@ -1320,16 +1320,6 @@ def test_namespaces_0
assert_equal( 1, XPath.match( d, "//x:*" ).size )
end
- def test_namespaces_cache
- doc = Document.new("")
- assert_equal("", XPath.first(doc, "//b[namespace-uri()='1']").to_s)
- assert_nil(XPath.first(doc, "//b[namespace-uri()='']"))
-
- doc.root.delete_namespace
- assert_nil(XPath.first(doc, "//b[namespace-uri()='1']"))
- assert_equal("", XPath.first(doc, "//b[namespace-uri()='']").to_s)
- end
-
def test_ticket_71
doc = Document.new(%Q{})
el = doc.root.elements[1]