aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/xml_mini/jdom.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support/xml_mini/jdom.rb')
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb183
1 files changed, 183 insertions, 0 deletions
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
new file mode 100644
index 0000000000..32fe6ade28
--- /dev/null
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+raise "JRuby is required to use the JDOM backend for XmlMini" unless RUBY_PLATFORM.include?("java")
+
+require "jruby"
+include Java
+
+require "active_support/core_ext/object/blank"
+
+java_import javax.xml.parsers.DocumentBuilder unless defined? DocumentBuilder
+java_import javax.xml.parsers.DocumentBuilderFactory unless defined? DocumentBuilderFactory
+java_import java.io.StringReader unless defined? StringReader
+java_import org.xml.sax.InputSource unless defined? InputSource
+java_import org.xml.sax.Attributes unless defined? Attributes
+java_import org.w3c.dom.Node unless defined? Node
+
+module ActiveSupport
+ module XmlMini_JDOM #:nodoc:
+ extend self
+
+ CONTENT_KEY = "__content__"
+
+ NODE_TYPE_NAMES = %w{ATTRIBUTE_NODE CDATA_SECTION_NODE COMMENT_NODE DOCUMENT_FRAGMENT_NODE
+ DOCUMENT_NODE DOCUMENT_TYPE_NODE ELEMENT_NODE ENTITY_NODE ENTITY_REFERENCE_NODE NOTATION_NODE
+ PROCESSING_INSTRUCTION_NODE TEXT_NODE}
+
+ node_type_map = {}
+ NODE_TYPE_NAMES.each { |type| node_type_map[Node.send(type)] = type }
+
+ # Parse an XML Document string or IO into a simple hash using Java's jdom.
+ # data::
+ # XML Document string or IO to parse
+ def parse(data)
+ if data.respond_to?(:read)
+ data = data.read
+ end
+
+ if data.blank?
+ {}
+ else
+ @dbf = DocumentBuilderFactory.new_instance
+ # secure processing of java xml
+ # https://archive.is/9xcQQ
+ @dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
+ @dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
+ @dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
+ @dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true)
+ xml_string_reader = StringReader.new(data)
+ xml_input_source = InputSource.new(xml_string_reader)
+ doc = @dbf.new_document_builder.parse(xml_input_source)
+ merge_element!({ CONTENT_KEY => "" }, doc.document_element, XmlMini.depth)
+ end
+ end
+
+ private
+
+ # Convert an XML element and merge into the hash
+ #
+ # hash::
+ # Hash to merge the converted element into.
+ # element::
+ # XML element to merge into hash
+ def merge_element!(hash, element, depth)
+ raise "Document too deep!" if depth == 0
+ delete_empty(hash)
+ merge!(hash, element.tag_name, collapse(element, depth))
+ end
+
+ def delete_empty(hash)
+ hash.delete(CONTENT_KEY) if hash[CONTENT_KEY] == ""
+ end
+
+ # Actually converts an XML document element into a data structure.
+ #
+ # element::
+ # The document element to be collapsed.
+ def collapse(element, depth)
+ hash = get_attributes(element)
+
+ child_nodes = element.child_nodes
+ if child_nodes.length > 0
+ (0...child_nodes.length).each do |i|
+ child = child_nodes.item(i)
+ merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE
+ end
+ merge_texts!(hash, element) unless empty_content?(element)
+ hash
+ else
+ merge_texts!(hash, element)
+ end
+ end
+
+ # Merge all the texts of an element into the hash
+ #
+ # hash::
+ # Hash to add the converted element to.
+ # element::
+ # XML element whose texts are to me merged into the hash
+ def merge_texts!(hash, element)
+ delete_empty(hash)
+ text_children = texts(element)
+ if text_children.join.empty?
+ hash
+ else
+ # must use value to prevent double-escaping
+ merge!(hash, CONTENT_KEY, text_children.join)
+ end
+ end
+
+ # Adds a new key/value pair to an existing Hash. If the key to be added
+ # already exists and the existing value associated with key is not
+ # an Array, it will be wrapped in an Array. Then the new value is
+ # appended to that Array.
+ #
+ # hash::
+ # Hash to add key/value pair to.
+ # key::
+ # Key to be added.
+ # value::
+ # Value to be associated with key.
+ def merge!(hash, key, value)
+ if hash.has_key?(key)
+ if hash[key].instance_of?(Array)
+ hash[key] << value
+ else
+ hash[key] = [hash[key], value]
+ end
+ elsif value.instance_of?(Array)
+ hash[key] = [value]
+ else
+ hash[key] = value
+ end
+ hash
+ end
+
+ # Converts the attributes array of an XML element into a hash.
+ # Returns an empty Hash if node has no attributes.
+ #
+ # element::
+ # XML element to extract attributes from.
+ def get_attributes(element)
+ attribute_hash = {}
+ attributes = element.attributes
+ (0...attributes.length).each do |i|
+ attribute_hash[CONTENT_KEY] ||= ""
+ attribute_hash[attributes.item(i).name] = attributes.item(i).value
+ end
+ attribute_hash
+ end
+
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
+ def texts(element)
+ texts = []
+ child_nodes = element.child_nodes
+ (0...child_nodes.length).each do |i|
+ item = child_nodes.item(i)
+ if item.node_type == Node.TEXT_NODE
+ texts << item.get_data
+ end
+ end
+ texts
+ end
+
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
+ def empty_content?(element)
+ text = +""
+ child_nodes = element.child_nodes
+ (0...child_nodes.length).each do |i|
+ item = child_nodes.item(i)
+ if item.node_type == Node.TEXT_NODE
+ text << item.get_data.strip
+ end
+ end
+ text.strip.length == 0
+ end
+ end
+end