3
# Copyright 2008 Amazon.com, Inc. or its affiliates. All Rights
4
# Reserved. Licensed under the Amazon Software License (the
5
# "License"). You may not use this file except in compliance with the
6
# License. A copy of the License is located at
7
# http://aws.amazon.com/asl or in the "license" file accompanying this
8
# file. This file is distributed on an "AS IS" BASIS, WITHOUT
9
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
10
# the License for the specific language governing permissions and
11
# limitations under the License.
13
# This class makes it easy to build up xml docs, xpath style.
16
# # Create an XMLBuilder:
17
# doc = REXML::Document.new(some_xml)
18
# builder = XMLBuilder.new(doc)
19
# # Add some text elements
20
# builder[/book/title] = 'Atlas Shrugged'
21
# builder[/book/author] = 'Ann Raynd'
23
# builder[/book/author@salutation] = 'Mrs.'
25
# Results in the following xml:
27
# <title>Atlas Shrugged</title>
28
# <author salutation="Mrs.">Ann Raynd</author>
32
# - If the path starts with a '/' the path is absolute.
33
# - If the path does not start with a '/' the path is relative.
34
# - When adding an element the return value is an xml builder object anchored at that path
35
# - When adding an attrubte the return value is ... ??? dunno what is should be. nil?
36
# - To set the element value use XMLBuilder[]=(string) or XMLBuilder[]=(XMLBuilder) or XMLBuilder[]=(node)
37
# - To add an element do builder[path] << string or builder[path] << element or builder[path] << node
38
# - If you assign a nil value the value will not be set and the path elements not created.
40
require 'rexml/document'
46
# Create a new XMLBuilder rooted at the given element, or at a new document if no root is given
47
def initialize(root = nil)
48
@root = root || REXML::Document.new()
51
# Retrieve a builder for the element at the given path
53
nodes = PathParser.parse(path)
54
rexml_node = nodes.inject(@root) do |rexml_node, parser_node|
55
parser_node.walk_visit(rexml_node)
57
XMLBuilder.new(nodes.last.retrieve_visit(rexml_node))
60
# Set the value of the element or attribute at the given path
62
# Don't create elements or make assignments for nil values
64
nodes = PathParser.parse(path)
65
rexml_node = nodes.inject(@root) do |rexml_node, parser_node|
66
parser_node.walk_visit(rexml_node)
68
nodes.last.assign_visit(rexml_node, value)
71
# Parses an xpath like expression
73
def PathParser.parse(path)
74
nodes = path.split('/')
78
while (nodes.length > 0)
79
node = Node.new(first, nodes)
81
@nodes << Document.new() if node.document
82
@nodes << Element.new(node.element, node.index) if node.element
83
@nodes << Attribute.new(node.attribute) if node.attribute
88
# Helper class used by PathParser
93
attr_reader :attribute
95
# Regex for parsing path if the form element[index]@attribute
96
# where [index] and @attribute are optional
97
@@attribute_regex = /^@(\w+)$/
98
@@node_regex = /^(\w+)(?:\[(\d+)\])?(?:@(\w+))?$/
100
# Nodes is path.split('/')
101
def initialize(allow_document, nodes)
102
if allow_document && nodes[0] == ''
107
nodes.shift while nodes[0] == ''
109
if (match = @@node_regex.match(node))
111
@index = match[2].to_i || 0
112
elsif (match = @@attribute_regex.match(node))
113
@attribute = match[1]
115
raise 'Invalid path: Node must be of the form element[index] or @attribute' if document
121
# Each class represents a node type. An array of these classes is built up when
122
# the path is parsed. Each node is then visited by the current rexml node and
123
# (depending on which visit function is called) the action taken.
125
# The visit function are:
126
# - walk: Walks down the xml dom
127
# - assign: Assigns the value to rexml node
128
# - retrieve: Retrives the value of the rexml node
130
# Different node types implement different behaviour types. For example retrieve is
131
# illegal on Attribute nodes, but on Document and Attribute nodes the given node is returned.
137
# Move to the document node (top of the xml dom)
138
def walk_visit(rexml_node)
139
return rexml_node if rexml_node.is_a?(REXML::Document)
140
raise "No document set on node. #{rexml_node.name}" if rexml_node.document == nil
144
def assign_visit(document_node, value)
145
raise 'Can only assign REXML::Elements to document nodes' if !value.is_a?(REXML::Element)
146
raise 'Expected a document node' if !document_node.is_a?(REXML::Element)
148
doc.replace_child(doc.root, value)
150
doc.add_element(element)
154
def retrieve_visit(document_node)
163
def initialize(name, index)
168
# Move one element down in the dom
169
def walk_visit(rexml_node)
170
elements = rexml_node.get_elements(name)
171
raise "Invalid index #{index} for element #{@name}" if @index > elements.length
172
# Create a node if it doesn't exist
173
if @index == elements.length
174
new_element = REXML::Element.new(@name)
175
rexml_node.add_element(new_element)
182
def assign_visit(rexml_node, value)
183
if value.is_a?(String)
184
rexml_node.text = value
185
elsif value.is_a?(REXML::Element)
186
value.name = rexml_node.name
187
raise "Node #{rexml_node.name} does not have a parent node" if rexml_node.parent.nil?
188
rexml_node.parent.replace_child(rexml_node, value)
190
raise 'Can only assign a String or a REXML::Element to an element'
194
def retrieve_visit(rexml_node)
206
# Stays on the same node in the dom
207
def walk_visit(rexml_node)
211
def assign_visit(rexml_node, value)
212
raise 'Can only assign an attribute to an element.' if !rexml_node.is_a?(REXML::Element)
213
rexml_node.attributes[@name] = value.to_s
216
def retrieve_visit(rexml_node)
217
raise 'Accessor not valid for paths with an attribute'
227
def assert_true(expr)
228
raise 'expected true' if !expr
231
doc = REXML::Document.new()
232
builder = XMLBuilder.new(doc)
233
root_builder = builder['/root']
234
root_builder['nested'] = 'more text'
235
root_builder['/root/bnode'] = 'name'
236
root_builder['/root/bnode/@attr'] = 'attr'