# # 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