Ruby [81] is a dynamic object-oriented programming language
known for its flexibility and uniformity when accessing object properties. Because
XML tree structures can easily be represented with Ruby objects, it is possible to
use a simple and intuitive way of dealing with XML objects using essentially the
same syntax as that used for other Ruby objects. In Ruby, XML processing is most
often performed using the REXML
package. In this section, we will show how
to use it for both DOM and SAX parsing, which we used in XML processing in Java.
To show how to process an existing XML structure, we will use the compacting
process that we programmed in Java in Section 8.1. In Ruby, DOM
parsing is achieved simply by creating an instance of the
REXML::Document
class. This instance is a node
and its
node type is given by its class: Element
or Text
in our
example. Information about a node is available through properties name
,
attributes
(a Hash
whose keys are the attribute
names), children
(an Array
that can be iterated upon with
each
) or value
(more useful for text nodes).
Example 10.1. [DOMCompact.rb
] Text compaction of the cellar book
(Example 2.2) with Ruby using the DOM model
Compare this program with Example 8.1.
1 require 'rexml/document'include REXML def compact(node,indent)
5 if(node.class==Element)
print node.name+"[" indent += ' '*(node.name.length+1); first=true; node.attributes.each do |key,value|
10 print "\n#{indent}" unless first print "@#{key}[#{value}]" first=false; end node.children.each do |child|
15 # deal only with element nodes or non-"empty" text nodes if child.class==Element || child.value.strip.length>0
print "\n#{indent}" unless first compact(child,indent) first=false; 20 end end print "]" elsif node.class==Text
print node.value.strip.gsub(/ *\n */," ") # normalize new lines 25 end end doc = Document.new(STDIN)
compact(doc.root,"")
30
|
Makes sure that the |
|
Prints the content of the XML node. Each line prefixed with an indentation, a string composed of the appropriate number of spaces. |
|
If the node is an element, prints the name of the node followed by an opening bracket and adds the appropriate number of spaces to the current indentation. |
|
Deals with attributes which are contained in a hash over which we
iterate. For each attribute, prints a |
|
Processes all children with an iterator. |
|
If it is an element or a non-empty text node, recursively calls
|
|
Processes a text node by printing it and normalizing internal newlines. |
|
Parses the file by creating a new XML document from the content of the standard input. |
|
Call the compacting process on the document node with an empty indentation string. |
In order to process an XML structure with the SAX approach, we will use a
similar compacting process to the one we programmed in Java in Section 8.2. SAX parsing is achieved by creating an instance of
the SAX2Parser
class to which we add a listener for parsing events. The
parsing process will send call-backs to the handler of these
events which will produce the compacted document.
Example 10.2. [SAXCompact.rb
] Text compaction of the cellar book
(Example 2.2) with Ruby using the SAX model
Compare this program with Example 8.3.
1 require 'rexml/document'require 'rexml/parsers/sax2parser' require 'rexml/sax2listener' require 'CompactHandler' 5 parser = REXML::Parsers::SAX2Parser.new(STDIN)
parser.listen(CompactHandler.new)
parser.parse
![]()
|
Makes sure that the appropriate SAX parser classes of the
|
|
Creates an instance of the SAX parser that will parse the standard input. |
|
Has the parser send its parsing events to the
|
|
Starts the parsing process. |
SAX parsing in Ruby is only a matter of defining the appropriate handlers.
Because the current version of REXML
does not handle XML entities,
some code is added to deal with their declaration and their expansion.
Example 10.3. [CompactHandler.rb
] Ruby SAX handler for text
compacting an XML file such as that of Example 2.2
Compare this program with Example 8.4.
1 class CompactHandler include REXML::SAX2Listenerdef initialize
@closed=false 5 @indent=0; @entities = {"&"=>"&", """=>'"', "'"=>"'", "<" => "<", ">" => ">"} end 10 def start_element(uri,localname,qname,attributes)
if @closed print "\n"+" "*@indent @closed=false end 15 @indent += 1+localname.length()
print localname+"[" first=true; attributes.each do |key,value|
print "\n"+" "*@indent if !first 20 first=false print "@#{key}[#{expandEntities(value)}]" @closed=true end end 25 def end_element(uri,localname,qname)
print "]" @closed=true @indent=@indent-1-localname.length 30 end def characters(text)
if text.strip.length>0 if @closed 35 print "\n"+" "*@indent @closed=false end text.strip! # remove leading and trailing space
text.gsub!(/[\s\r\n]+/,' ') # normalize space 40 print expandEntities(text) @closed=true end end 45 def entitydecl(name,decl)
@entities["&"+name+";"]=decl end def expandEntities(text)
50 if text.include?("&") # match entities as long as there are replacements @entities.each{|key,value| retry if text.gsub!(key,value)} # replace numerical entities starting with &# while true 55 if res=text.match("&#x([0-9a-fA-F]+);") # hexadecimal
text=res.pre_match+[res[1].hex].pack("U")+res.post_match elsif res=text.match("&#([0-9]+);") # decimal text=res.pre_match+[res[1].to_i].pack("U")+res.post_match else 60 break end end end text 65 end end
|
Includes the default declarations needed to handle events sent by a SAX parser. |
|
Defines and initializes two instance variables needed to output the element with the correct indentation. Creates the entities table and initializes it with the predefined XML entities. |
|
When a new element is started, finishes the current indentation if needed. |
|
Updates the current indentation by adding the length of the element name. |
|
Prints the first attribute on the same line and the others on the subsequent lines properly indented. |
|
When an element finishes, closes the current bracket and updates the current indentation. |
|
For a non-empty text node, ends current line if needed. |
|
Removes leading and trailing space, normalizes and expands entities. |
|
When a new entity is declared, add it to the entities table. |
|
If the text contains an ampersand, it means that it contains entities the program must deal with. Entities from the entities table are expanded as often as it is necessary. |
|
Numerical entities are expanded by replacing them with their Unicode equivalent. |
In order to demonstrate the creation of new XML document, we will parse the
compact form produced in Section 10.1.1 or in Section 10.1.2 like we did in Chapter 9.
We first need a way to access appropriate tokens corresponding
to important signals in the input file. This is achieved by defining a
CompactTokenizer
class (Example 10.4) that
will return opening and closing square brackets, at-signs and the rest as a single
string. Newlines will also delimit tokens but will be ignored in the document
processing. The file is processed by a series of calls to nextToken
or
skip
. skip
provides a useful redundancy check for
tokens that are encountered in the input but are ignored for output.
Example 10.4. [CompactTokenizer.rb
] Specialized string scanner that
returns tokens of compact form.
Compare this program with Example 9.1.
1 require 'strscan'class CompactTokenizer
attr_reader :token
5 def initialize(file)
@re = /\[|\]|\n|@|[^\[\]\n@]+/ # handle different token types @scanner = StringScanner.new(file.read) end 10 # get next non-whitespace token and return it def nextToken
loop do @token = @scanner.scan(@re) break if @token !~ /^\n? *$/ #skip tokens with only whitespace 15 end @token end # if the current token is "sym" then get next one 20 # otherwise, output error message def skip(sym)
if (@token==sym) nextToken else 25 # output error message giving the position and the current line str = @scanner.string pos = @scanner.pos raise "skip:#{sym} expected but #{@token} found at position #{pos}:"+ str[str.rindex("\n",pos)||0 ... str.index("\n",pos)||str.length] 30 end end end
|
Includes the |
|
Defines the |
|
Makes the current token available as a readable attribute. |
|
|
|
Gets the next token but skip those containing only whitespace. It returns the value of the current token. |
|
Checks that the current token is the same as the one given as parameter and retrieves the next one. If the current token does not match, then raises an exception giving the expected token and the one found with the content of the current line. |
Thanks to the dynamic object oriented aspect of Ruby, much of the complexity of
node creation and child addition is hidden in simple class instance creations:
Document.new()
, Element.new()
and
Text.new()
. Adding a new node is done with the
<<
operator that appends new information at the end of almost
any object in Ruby. The attributes of a node are kept in a hash table that is an a
property of an XML node. This hash table is assigned like any other one in Ruby.
Example 10.5. [DOMExpand.rb
] Ruby compact form parsing to create an
XML document
A sample input for this program is Example 5.8 , which should yield Example 2.2. Compare this program with Example 9.2.
1 require 'rexml/document'include REXML require 'compactTokenizer'
5 st = CompactTokenizer.new(STDIN)
# start new document doc = Document.new()
doc << XMLDecl.new(1.0,"UTF-8")
10 # find the name of the root rootName = 'dummyElement'
while st.nextToken!='[' rootName=st.token end 15 # create the element and return it with the current token def expand(st,elementName)
elem = Element.new(elementName)
st.skip("[") 20 while st.token=='@' # process attributes
attName = st.skip("@") st.nextToken elem.attributes[attName] = st.skip("[")
st.nextToken 25 st.skip("]") end while st.token!=']' # process children
s = st.token.strip st.nextToken 30 if(st.token=='[') elem << expand(st,s)
else elem << Text.new(s)
end 35 end st.skip("]") return elem end 40 # expand from the root element doc << expand(st,rootName)
# write it properly indented doc.write(STDOUT,0)
![]()
|
Ensures that the library |
|
Ensures that the |
|
Creates the tokenizer on the standard input. |
|
Initializes a new XML document. |
|
Adds an appropriate XML declaration. |
|
The name of the root is the first token immediately followed by an opening square bracket. |
|
Creates an XML element corresponding to the content of
|
|
Creates a new, empty XML element with name
|
|
Because all attribute names start with an |
|
Adding an attribute is done by assigning to the
|
|
All children are then processed in turn until a closing square
bracket is encountered. |
|
A child node is expanded by a recursive call whose result is added as the last child of the current element. |
|
A text node is added as the last child of the current element. |
|
Calls the expansion of the document starting from the
|
|
Serializes the document on the standard output. By default, the XML is properly indented. The second parameter gives a global indentation level if needed. |