# Copyright 2008-2014 Amazon.com, Inc. or its affiliates. All Rights # Reserved. Licensed under the Amazon Software License (the # "License"). You may not use this file except in compliance with the # License. A copy of the License is located at # http://aws.amazon.com/asl or in the "license" file accompanying this # file. This file is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See # the License for the specific language governing permissions and # limitations under the License. # This class makes it easy to build up xml docs, xpath style. # Usage: # # # Create an XMLBuilder: # doc = REXML::Document.new(some_xml) # builder = XMLBuilder.new(doc) # # Add some text elements # builder[/book/title] = 'Atlas Shrugged' # builder[/book/author] = 'Ann Raynd' # # Add an attribute # builder[/book/author@salutation] = 'Mrs.' # # Results in the following xml: # # Atlas Shrugged # Ann Raynd # # # Notes on Usage: # - If the path starts with a '/' the path is absolute. # - If the path does not start with a '/' the path is relative. # - When adding an element the return value is an xml builder object anchored at that path # - When adding an attrubte the return value is ... ??? dunno what is should be. nil? # - To set the element value use XMLBuilder[]=(string) or XMLBuilder[]=(XMLBuilder) or XMLBuilder[]=(node) # - To add an element do builder[path] << string or builder[path] << element or builder[path] << node # - If you assign a nil value the value will not be set and the path elements not created. require 'rexml/document' class XMLBuilder attr_reader :root # Create a new XMLBuilder rooted at the given element, or at a new document if no root is given def initialize(root = nil) @root = root || REXML::Document.new() end # Retrieve a builder for the element at the given path def [](path) nodes = PathParser.parse(path) rexml_node = nodes.inject(@root) do |rexml_node, parser_node| parser_node.walk_visit(rexml_node) end XMLBuilder.new(nodes.last.retrieve_visit(rexml_node)) end # Set the value of the element or attribute at the given path def []=(path, value) # Don't create elements or make assignments for nil values return if value.nil? nodes = PathParser.parse(path) rexml_node = nodes.inject(@root) do |rexml_node, parser_node| parser_node.walk_visit(rexml_node) end nodes.last.assign_visit(rexml_node, value) end # Parses an xpath like expression class PathParser def PathParser.parse(path) nodes = path.split('/') @nodes = [] first = true while (nodes.length > 0) node = Node.new(first, nodes) first = false @nodes << Document.new() if node.document @nodes << Element.new(node.element, node.index) if node.element @nodes << Attribute.new(node.attribute) if node.attribute end @nodes end # Helper class used by PathParser class Node attr_reader :document attr_reader :element attr_reader :index attr_reader :attribute # Regex for parsing path if the form element[index]@attribute # where [index] and @attribute are optional @@attribute_regex = /^@(\w+)$/ @@node_regex = /^(\w+)(?:\[(\d+)\])?(?:@(\w+))?$/ # Nodes is path.split('/') def initialize(allow_document, nodes) if allow_document && nodes[0] == '' @document = true nodes.shift return end nodes.shift while nodes[0] == '' node = nodes.shift if (match = @@node_regex.match(node)) @element = match[1] @index = match[2].to_i || 0 elsif (match = @@attribute_regex.match(node)) @attribute = match[1] else raise 'Invalid path: Node must be of the form element[index] or @attribute' if document end end end # Node classes # Each class represents a node type. An array of these classes is built up when # the path is parsed. Each node is then visited by the current rexml node and # (depending on which visit function is called) the action taken. # # The visit function are: # - walk: Walks down the xml dom # - assign: Assigns the value to rexml node # - retrieve: Retrives the value of the rexml node # # Different node types implement different behaviour types. For example retrieve is # illegal on Attribute nodes, but on Document and Attribute nodes the given node is returned. class Document def initialize() end # Move to the document node (top of the xml dom) def walk_visit(rexml_node) return rexml_node if rexml_node.is_a?(REXML::Document) raise "No document set on node. #{rexml_node.name}" if rexml_node.document == nil rexml_node.document end def assign_visit(document_node, value) raise 'Can only assign REXML::Elements to document nodes' if !value.is_a?(REXML::Element) raise 'Expected a document node' if !document_node.is_a?(REXML::Element) if doc.root doc.replace_child(doc.root, value) else doc.add_element(element) end end def retrieve_visit(document_node) document_node end end class Element attr_reader :name attr_reader :index def initialize(name, index) @name = name @index = index end # Move one element down in the dom def walk_visit(rexml_node) elements = rexml_node.get_elements(name) raise "Invalid index #{index} for element #{@name}" if @index > elements.length # Create a node if it doesn't exist if @index == elements.length new_element = REXML::Element.new(@name) rexml_node.add_element(new_element) new_element else elements[@index] end end def assign_visit(rexml_node, value) if value.is_a?(String) rexml_node.text = value elsif value.is_a?(REXML::Element) value.name = rexml_node.name raise "Node #{rexml_node.name} does not have a parent node" if rexml_node.parent.nil? rexml_node.parent.replace_child(rexml_node, value) else raise 'Can only assign a String or a REXML::Element to an element' end end def retrieve_visit(rexml_node) rexml_node end end class Attribute attr_reader :name def initialize(name) @name = name end # Stays on the same node in the dom def walk_visit(rexml_node) rexml_node end def assign_visit(rexml_node, value) raise 'Can only assign an attribute to an element.' if !rexml_node.is_a?(REXML::Element) rexml_node.attributes[@name] = value.to_s end def retrieve_visit(rexml_node) raise 'Accessor not valid for paths with an attribute' end end end end # Test code if $0 == __FILE__ def assert_true(expr) raise 'expected true' if !expr end doc = REXML::Document.new() builder = XMLBuilder.new(doc) root_builder = builder['/root'] root_builder['nested'] = 'more text' root_builder['/root/bnode'] = 'name' root_builder['/root/bnode/@attr'] = 'attr' puts doc end