~awstools-dev/ubuntu/maverick/ec2-ami-tools/maverick

« back to all changes in this revision

Viewing changes to lib/ec2/amitools/xmlbuilder.rb

  • Committer: Bazaar Package Importer
  • Author(s): Chuck Short
  • Date: 2008-10-14 08:35:25 UTC
  • Revision ID: james.westby@ubuntu.com-20081014083525-c0n69wr7r7aqfb8w
Tags: 1.3-26357-0ubuntu2
* New upstream version.
* Update the debian copyright file.
* Added quilt patch system to make it easier to maintain. 

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/bin/env ruby
 
2
 
 
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.
 
12
 
 
13
# This class makes it easy to build up xml docs, xpath style.
 
14
# Usage:
 
15
#
 
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'
 
22
# # Add an attribute
 
23
# builder[/book/author@salutation] = 'Mrs.'
 
24
#
 
25
# Results in the following xml:
 
26
# <book>
 
27
#   <title>Atlas Shrugged</title>
 
28
#   <author salutation="Mrs.">Ann Raynd</author>
 
29
# </book>
 
30
#
 
31
# Notes on Usage:
 
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.
 
39
 
 
40
require 'rexml/document'
 
41
 
 
42
class XMLBuilder
 
43
 
 
44
  attr_reader :root
 
45
  
 
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()
 
49
  end
 
50
 
 
51
  # Retrieve a builder for the element at the given path
 
52
  def [](path)
 
53
    nodes = PathParser.parse(path)
 
54
    rexml_node = nodes.inject(@root) do |rexml_node, parser_node|
 
55
      parser_node.walk_visit(rexml_node)
 
56
    end
 
57
    XMLBuilder.new(nodes.last.retrieve_visit(rexml_node))
 
58
  end
 
59
 
 
60
  # Set the value of the element or attribute at the given path
 
61
  def []=(path, value)
 
62
    # Don't create elements or make assignments for nil values
 
63
    return if value.nil?
 
64
    nodes = PathParser.parse(path)
 
65
    rexml_node = nodes.inject(@root) do |rexml_node, parser_node|
 
66
      parser_node.walk_visit(rexml_node)
 
67
    end
 
68
    nodes.last.assign_visit(rexml_node, value)
 
69
  end
 
70
 
 
71
  # Parses an xpath like expression
 
72
  class PathParser
 
73
    def PathParser.parse(path)
 
74
      nodes = path.split('/')
 
75
      @nodes = []
 
76
      first = true
 
77
 
 
78
      while (nodes.length > 0)
 
79
        node = Node.new(first, nodes)
 
80
        first = false
 
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
 
84
      end
 
85
      @nodes
 
86
    end
 
87
 
 
88
    # Helper class used by PathParser
 
89
    class Node
 
90
      attr_reader :document
 
91
      attr_reader :element
 
92
      attr_reader :index
 
93
      attr_reader :attribute
 
94
 
 
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+))?$/
 
99
 
 
100
      # Nodes is path.split('/')
 
101
      def initialize(allow_document, nodes)
 
102
        if allow_document && nodes[0] == ''
 
103
          @document = true
 
104
          nodes.shift
 
105
          return
 
106
        end
 
107
        nodes.shift while nodes[0] == ''
 
108
        node = nodes.shift
 
109
        if (match = @@node_regex.match(node))
 
110
          @element = match[1]
 
111
          @index = match[2].to_i || 0
 
112
        elsif (match = @@attribute_regex.match(node))
 
113
          @attribute = match[1]
 
114
        else
 
115
          raise 'Invalid path: Node must be of the form element[index] or @attribute' if document
 
116
        end
 
117
      end
 
118
    end
 
119
 
 
120
    # Node classes
 
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.
 
124
    #
 
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
 
129
    #
 
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.
 
132
 
 
133
    class Document
 
134
      def initialize()
 
135
      end
 
136
 
 
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
 
141
        rexml_node.document
 
142
      end
 
143
 
 
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)
 
147
        if doc.root
 
148
          doc.replace_child(doc.root, value)
 
149
        else
 
150
          doc.add_element(element)
 
151
        end
 
152
      end
 
153
 
 
154
      def retrieve_visit(document_node)
 
155
        document_node
 
156
      end
 
157
    end
 
158
 
 
159
    class Element
 
160
      attr_reader :name
 
161
      attr_reader :index
 
162
 
 
163
      def initialize(name, index)
 
164
        @name = name
 
165
        @index = index
 
166
      end
 
167
 
 
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)
 
176
          new_element
 
177
        else
 
178
          elements[@index]
 
179
        end
 
180
      end
 
181
 
 
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)
 
189
        else
 
190
          raise 'Can only assign a String or a REXML::Element to an element'
 
191
        end
 
192
      end
 
193
 
 
194
      def retrieve_visit(rexml_node)
 
195
        rexml_node
 
196
      end
 
197
    end
 
198
 
 
199
    class Attribute
 
200
      attr_reader :name
 
201
 
 
202
      def initialize(name)
 
203
        @name = name
 
204
      end
 
205
 
 
206
      # Stays on the same node in the dom
 
207
      def walk_visit(rexml_node)
 
208
        rexml_node
 
209
      end
 
210
 
 
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
 
214
      end
 
215
      
 
216
      def retrieve_visit(rexml_node)
 
217
        raise 'Accessor not valid for paths with an attribute'
 
218
      end
 
219
    end
 
220
  end
 
221
 
 
222
end
 
223
 
 
224
# Test code
 
225
if $0 == __FILE__
 
226
  
 
227
  def assert_true(expr)
 
228
    raise 'expected true' if !expr
 
229
  end
 
230
  
 
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'
 
237
  puts doc
 
238
end
 
239