491 lines
13 KiB
Ruby
Executable File
491 lines
13 KiB
Ruby
Executable File
#
|
|
# File:: xmlserial.rb
|
|
# Description:: Object XML serialisation methods.
|
|
#
|
|
# Date:: 21 July 2008
|
|
#
|
|
=begin
|
|
---------------------------------------------------------------------------
|
|
Copyright (c) 2002-2003, Chris Morris
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are
|
|
met:
|
|
|
|
1. Redistributions of source code must retain the above copyright
|
|
notice, this list of conditions and the following disclaimer.
|
|
|
|
2. Redistributions in binary form must reproduce the above copyright
|
|
notice, this list of conditions and the following disclaimer in the
|
|
documentation and/or other materials provided with the distribution.
|
|
|
|
3. Neither the names Chris Morris, cLabs nor the names of contributors
|
|
to this software may be used to endorse or promote products derived from
|
|
this software without specific prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
|
|
IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
|
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
---------------------------------------------------------------------------
|
|
Contributors:
|
|
Harry Ohlsen [harryo@zip.com.au]
|
|
Stefan Mueller [flcl@gmx.net]
|
|
=end
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Uses
|
|
#-----------------------------------------------------------------------------
|
|
require 'rexml/document'
|
|
require 'singleton'
|
|
require 'parsedate'
|
|
begin
|
|
require 'time'
|
|
rescue LoadError
|
|
# don't do anything
|
|
end
|
|
|
|
|
|
class Object
|
|
def make_type_element
|
|
element = REXML::Element.new(self.class.name.gsub('::', '-'))
|
|
end
|
|
|
|
# this method mainly gets things in order with the XML element
|
|
# then calls instance_data_to_xml to actually get the data
|
|
def to_xml(parentElement=nil)
|
|
if parentElement == nil
|
|
element = make_type_element
|
|
else
|
|
if XSConf.outputTypeElements || type_elements_required?
|
|
element = make_type_element
|
|
parentElement.add_element(element)
|
|
else
|
|
element = parentElement
|
|
end
|
|
end
|
|
instance_data_to_xml(element)
|
|
element
|
|
end
|
|
|
|
def instance_data_to_xml(element)
|
|
raise "instance_data_to_xml must be defined for " + self.type.name
|
|
end
|
|
|
|
# Descendant classes can override this to force type elements
|
|
# to be output. Array and Hash use this depending on their contents
|
|
def type_elements_required?
|
|
false
|
|
end
|
|
end
|
|
|
|
class Class
|
|
def new_without_initialize( *args )
|
|
self.class_eval %{
|
|
alias :old_initialize_with_args :initialize
|
|
def initialize( *args ); end
|
|
}
|
|
begin
|
|
result = self.new( *args )
|
|
ensure
|
|
self.class_eval %{
|
|
undef :initialize
|
|
alias :initialize :old_initialize_with_args
|
|
}
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
|
|
# Singleton configuration class. See XSConf.
|
|
# outputTypeElements:: boolean value that controls whether or not type
|
|
# elements are output in the XML. Default is true.
|
|
# timeFormat:: format string used for input/output of Time types. Default
|
|
# is %Y-%b-%d %H:%M:%S
|
|
# bypassInitialize:: when creating class instances during from_xml calls,
|
|
# setting bypassInitialize to true will not call the
|
|
# initialize method. This allows classes with parameterized
|
|
# initializers to be instantiated. Default is false.
|
|
class XmlSerialConf
|
|
include Singleton
|
|
|
|
attr_accessor :outputTypeElements, :timeFormat, :bypassInitialize
|
|
|
|
def initialize
|
|
@outputTypeElements = true
|
|
@bypassInitialize = false
|
|
@timeFormat = '%Y-%b-%d %H:%M:%S'
|
|
end
|
|
end
|
|
|
|
# convenience constant to refer to singleton XmlSerialConf class
|
|
XSConf = XmlSerialConf.instance
|
|
|
|
# Utility singleton methods for internal use
|
|
class XmlSerialUtil
|
|
@@class_aliases = {}
|
|
|
|
# method to convert XML element text from String into the proper
|
|
# simple type. For example, the string "5" is converted to
|
|
# Fixnum 5
|
|
def XmlSerialUtil.convertSimpleType(value)
|
|
# Many thanks to Dave Thomas for this one. Saved me from regexp hell.
|
|
Integer(value) rescue Float(value) rescue value
|
|
end
|
|
|
|
# utility method for finding a class within a module hierarchy
|
|
def XmlSerialUtil.find_class(name)
|
|
name = @@class_aliases[name] if @@class_aliases[name]
|
|
subclasses = name.gsub(/-/, "::").split("::")
|
|
|
|
c = Object
|
|
|
|
subclasses.each do |s|
|
|
c = c.const_get(s)
|
|
end
|
|
c
|
|
end
|
|
|
|
# setup class aliases in case the xml element names don't
|
|
# lend themselves easily to clear class names
|
|
def XmlSerialUtil.set_class_alias(elementName, className)
|
|
@@class_aliases[elementName] = className
|
|
end
|
|
end
|
|
|
|
# convenience constant to refer to XmlSerialUtil
|
|
XSUtil = XmlSerialUtil
|
|
|
|
class XmlSerialCyclicalReferenceCop
|
|
include Singleton
|
|
|
|
def initialization
|
|
@idlist = []
|
|
end
|
|
|
|
def police_id(id)
|
|
if @idlist.include?(id)
|
|
false
|
|
else
|
|
@idlist << id
|
|
true
|
|
end
|
|
end
|
|
end
|
|
|
|
module XmlSerialization
|
|
# refactoring -- make a generic REXML wrapper in a separate unit
|
|
# that would allow others to more easily substitute in their own
|
|
# xml parsing engine
|
|
|
|
# called by to_xml method added to the Object class.
|
|
def instance_data_to_xml(element)
|
|
instance_variables.each do |instanceVarName|
|
|
instanceVarName.sub!(/@/, '')
|
|
instanceVarName.sub!(/::/, '-')
|
|
instanceVarValue = self.instance_eval "@#{instanceVarName}"
|
|
if instanceVarValue != nil
|
|
instanceElement = element.add_element(instanceVarName)
|
|
self.instance_eval "instanceValue = (@#{instanceVarName}).to_xml(instanceElement)"
|
|
end
|
|
end
|
|
end
|
|
|
|
def XmlSerialization.append_features(includingClass)
|
|
# [ruby-talk:14976] - append_features makes from_xml a
|
|
# singleton/class method in the including class. The call to super is
|
|
# required to make this work properly
|
|
super
|
|
def includingClass.from_xml(element)
|
|
if XSConf.bypassInitialize
|
|
if VERSION =~ /1\.6|1\.7/
|
|
obj = self.new_without_initialize
|
|
else
|
|
obj = self.allocate
|
|
end
|
|
else
|
|
obj = new
|
|
end
|
|
element.elements.each do |instanceElement|
|
|
instanceVarName = instanceElement.name
|
|
if instanceElement.has_elements?
|
|
childElement = instanceElement.elements[1]
|
|
typeName = childElement.name
|
|
else
|
|
childElement = instanceElement
|
|
instanceVar = obj.instance_eval "@#{instanceVarName}"
|
|
if instanceVar.instance_of? NilClass
|
|
value = XSUtil.convertSimpleType(instanceElement.text)
|
|
obj.instance_eval "@#{instanceVarName} = value"
|
|
next
|
|
else
|
|
typeName = instanceVar.class.name
|
|
end
|
|
end
|
|
value = XSUtil.find_class(typeName).from_xml(childElement)
|
|
obj.instance_eval "@#{instanceVarName} = value"
|
|
end
|
|
obj
|
|
end
|
|
end
|
|
end
|
|
|
|
# all from_xml are class methods, because self modifying instances
|
|
# are a hassle, and not needed
|
|
|
|
class String
|
|
def to_xml_text
|
|
# String is duped in case it's frozen. Xml processing might alter it
|
|
# by removing white-space, etc.
|
|
self.dup
|
|
end
|
|
|
|
def instance_data_to_xml(element)
|
|
element.add_text(to_xml_text)
|
|
end
|
|
|
|
def String.from_xml(element)
|
|
if element.text != nil
|
|
# puts 'element.text.tainted? = ' + element.text.tainted?.to_s
|
|
String.new(element.text)
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
class Numeric
|
|
def to_xml_text
|
|
self.to_s
|
|
end
|
|
|
|
def instance_data_to_xml(element)
|
|
element.add_text(to_xml_text)
|
|
end
|
|
end
|
|
|
|
class Integer
|
|
def Integer.from_xml(element)
|
|
element.text.to_i
|
|
end
|
|
end
|
|
|
|
class Float
|
|
def Float.from_xml(element)
|
|
element.text.to_f
|
|
end
|
|
end
|
|
|
|
class Time
|
|
def to_xml_text
|
|
self.strftime(XSConf.timeFormat)
|
|
end
|
|
|
|
def instance_data_to_xml(element)
|
|
element.add_text(to_xml_text)
|
|
end
|
|
|
|
def Time.from_xml(element)
|
|
# time.rb added in Ruby 1.6.7
|
|
if $".include?('time.rb')
|
|
Time.parse(element.text)
|
|
else
|
|
Time.local(*ParseDate.parsedate(element.text)[0..5])
|
|
end
|
|
end
|
|
end
|
|
|
|
class Array
|
|
def type_elements_required?
|
|
!all_items_types_support_no_type_elements?
|
|
end
|
|
|
|
def type_supports_no_type_element?(item)
|
|
(item.kind_of? String) || (item.kind_of? Numeric)
|
|
end
|
|
|
|
def all_items_types_support_no_type_elements?
|
|
result = true
|
|
each do |item|
|
|
if !type_supports_no_type_element?(item)
|
|
result = false
|
|
break
|
|
end
|
|
end
|
|
result
|
|
end
|
|
|
|
def Array.no_type_elements_delimiter
|
|
","
|
|
end
|
|
|
|
def instance_data_to_xml(element)
|
|
outputTypeElements =
|
|
XSConf.outputTypeElements || type_elements_required?
|
|
|
|
if outputTypeElements
|
|
orig = XSConf.outputTypeElements
|
|
XSConf.outputTypeElements = true
|
|
each do |item|
|
|
item.to_xml(element)
|
|
end
|
|
XSConf.outputTypeElements = orig
|
|
else
|
|
text = ''
|
|
each do |item|
|
|
text = text + Array.no_type_elements_delimiter if !text.empty?
|
|
text = text + item.to_xml_text
|
|
end
|
|
element.add_text(text)
|
|
end
|
|
end
|
|
|
|
def Array.from_xml(element)
|
|
result = []
|
|
if element.has_elements?
|
|
element.elements.each do |itemElement| # itemElement = '<[type]>'
|
|
childElement = itemElement
|
|
typeName = childElement.name
|
|
result << XSUtil.find_class(typeName).from_xml(childElement)
|
|
end
|
|
else
|
|
text = element.text
|
|
if text != nil
|
|
result = text.split(Array.no_type_elements_delimiter)
|
|
result.collect! do |item| XSUtil.convertSimpleType(item) end
|
|
end
|
|
end
|
|
result
|
|
end
|
|
end
|
|
|
|
class Hash
|
|
def type_elements_required?
|
|
!all_items_types_support_no_type_elements?
|
|
end
|
|
|
|
# refactor? Copy of method in Array, but where is a one-time place for it?
|
|
def type_supports_no_type_element?(item)
|
|
(item.kind_of? String) || (item.kind_of? Numeric)
|
|
end
|
|
|
|
def all_items_types_support_no_type_elements?
|
|
result = true
|
|
each do |key, value|
|
|
if !type_supports_no_type_element?(key)
|
|
result = false
|
|
break
|
|
end
|
|
|
|
if !type_supports_no_type_element?(value)
|
|
result = false
|
|
break
|
|
end
|
|
end
|
|
result
|
|
end
|
|
|
|
def Hash.pair_delimiter
|
|
","
|
|
end
|
|
|
|
def Hash.key_value_delimiter
|
|
"="
|
|
end
|
|
|
|
def instance_data_to_xml(element)
|
|
outputTypeElements =
|
|
XSConf.outputTypeElements || type_elements_required?
|
|
|
|
if outputTypeElements
|
|
orig = XSConf.outputTypeElements
|
|
XSConf.outputTypeElements = true
|
|
each do |key, value|
|
|
pairElement = REXML::Element.new('Pair')
|
|
element.add_element(pairElement)
|
|
|
|
keyElement = REXML::Element.new('Key')
|
|
pairElement.add_element(keyElement)
|
|
key.to_xml(keyElement)
|
|
|
|
valueElement = REXML::Element.new('Value')
|
|
pairElement.add_element(valueElement)
|
|
value.to_xml(valueElement)
|
|
end
|
|
XSConf.outputTypeElements = orig
|
|
else
|
|
text = ''
|
|
each do |key, value|
|
|
text = text + Hash.pair_delimiter if !text.empty?
|
|
text = text + key.to_xml_text + Hash.key_value_delimiter + value.to_xml_text
|
|
end
|
|
element.add_text(text)
|
|
end
|
|
end
|
|
|
|
def Hash.from_xml(element)
|
|
result = {}
|
|
if element.has_elements?
|
|
element.each_element do |pairElement|
|
|
keyElement = pairElement.elements[1]
|
|
keyTypeElement = keyElement.elements[1]
|
|
key = XSUtil.find_class(keyTypeElement.name).from_xml(keyTypeElement)
|
|
|
|
valueElement = pairElement.elements[2]
|
|
valueTypeElement = valueElement.elements[1]
|
|
value = XSUtil.find_class(valueTypeElement.name).from_xml(valueTypeElement)
|
|
|
|
result[key] = value
|
|
end
|
|
else
|
|
text = element.text
|
|
if text != nil
|
|
ary = text.split(Hash.pair_delimiter)
|
|
ary.each do |pair|
|
|
key, value = pair.split(Hash.key_value_delimiter)
|
|
key = XSUtil.convertSimpleType(key)
|
|
value = XSUtil.convertSimpleType(value)
|
|
result[key] = value
|
|
end
|
|
end
|
|
end
|
|
result
|
|
end
|
|
end
|
|
|
|
class TrueClass
|
|
def to_xml_text
|
|
self.to_s
|
|
end
|
|
def instance_data_to_xml(element)
|
|
element.add_text(to_xml_text)
|
|
end
|
|
def TrueClass.from_xml(element)
|
|
true
|
|
end
|
|
end
|
|
|
|
class FalseClass
|
|
def to_xml_text
|
|
self.to_s
|
|
end
|
|
def instance_data_to_xml(element)
|
|
element.add_text(to_xml_text)
|
|
end
|
|
def FalseClass.from_xml(element)
|
|
false
|
|
end
|
|
end
|
|
|
|
# End of xmlserial.rb
|